关于 experience-lwc-typescript-migrate
用于将 Lightning Web Components(LWC)从 JavaScript 迁移到 TypeScript 的技能,面向维护 Salesforce 组件的开发者。它强调保留 Git 历史、为公开的 @api 接口生成仅含公开成员的 .d.ts,并通过编译和测试验证。
主要功能
- 01将组件和辅助文件重命名为 .ts
- 02为公开及内部成员添加类型
- 03生成仅包含 @api 成员的 .d.ts
- 04检查事件与异步方法类型
- 05运行 TypeScript 编译和 Jest 测试
适用场景
- 01将单个 LWC 从 JavaScript 迁移到 TypeScript
- 02为现有 LWC 创建公开类型声明
- 03补全已重命名组件的 TypeScript 类型
安装
复制下面的命令,在终端中运行。
npx skills add https:/ / github.com/ forcedotcom/ sf-skills --skill experience-lwc-typescript-migrate安装后在 Claude Code 中输入/experience-lwc-typescript-migrate确认已加载。
包含的文件
除 SKILL.md 外,安装时一起下载的参考资料和脚本。
assets/dts-template.tstype-patterns.tsscripts/find-consumers.sh详细介绍
将 LWC 转换为 TypeScript
将 Lightning Web Component 包从 JavaScript 转换为 TypeScript。交付内容为一份类型完整的 .ts 实现,以及一个仅公开 @api 成员的 .d.ts 文件(其他 LWC 会使用这些公共接口)。
何时使用此技能
- 用户想将单个组件或一个组件文件夹从
.js迁移到.ts。 - 用户需要为现有 LWC 编写
.d.ts,以便其他组件(或外部 TypeScript 宿主)安全地导入它。 - 用户正在为已重命名为
.ts、但尚未正确添加类型的 LWC 添加类型注解。 - 用户想将 JSDoc 风格的类型提示升级为真正的 TypeScript 类型。
前置条件
- 组件目前使用 JavaScript 时可以正确构建和运行。
- 系统中有可用的
git(重命名必须通过git mv保留历史记录)。 - 构建流程中已接入 TypeScript 编译器(SFDX TS 流程或独立的
tsc步骤均可)。
工作流程
步骤 1 — 阅读组件
打开组件包中的每个文件:
componentName/
├── componentName.js
├── componentName.html
├── componentName.css
└── (possibly) __tests__/, __utam__/, existing .d.ts
了解以下内容:
- 哪些类继承了
LightningElement?类名是什么? - 哪些字段和方法带有
@api装饰器? - 哪些属性或方法已有 JSDoc(可将其作为类型提示的起点,但要根据实际用法进行验证——JSDoc 可能不准确)。
- 根据代码内部的调用方式,可以推断出哪些参数和返回类型?
步骤 2 — 使用 git mv 将 .js 重命名为 .ts
git mv componentName/componentName.js componentName/componentName.ts
对组件包中的任何辅助 .js 文件重复此操作(除非它们已经是 .ts)。绝不要直接使用 mv——这会丢失 TypeScript 审核人员依赖的历史关联。
步骤 3 — 在 .ts 中添加类型注解
按以下优先级顺序添加类型;一旦公共契约完整,就可以停止:
- 先处理
@api属性和方法。 如果缺少 JSDoc,就生成 JSDoc,然后将 JSDoc 类型转换为 TS 语法(string、number、boolean、Promise<T>)。在采信每条 JSDoc 描述之前,都要先根据代码进行验证。 - 复杂结构应定义为
interface或type别名,不要在各处重复写内联结构。 - 仅当值确实允许为
undefined时,才使用?表示可选成员。 不要出于防范而到处添加?。 - 私有或内部状态——仍需标注类型,但不要导出这些类型。对于绝不能由使用方访问的成员,使用
private。 - 事件处理程序——优先使用精确的 DOM 事件类型:
- 对
onclick(以及其他类似点击的处理程序)使用MouseEvent。click会作为MouseEvent分派——包括由键盘触发的点击——因此将其标注为PointerEvent,会让处理程序依赖在这些情况下未定义的指针专属字段(pointerType、pressure等)。 - 对
onpointerdown/onpointerup/onpointermove以及其他确实需要指针专属字段的pointer*处理程序,使用PointerEvent。 - 对 LWC 自定义事件,使用
CustomEvent<{ detail: ... }>。 Event是最后的选择;使用时请说明原因。
- 对
- 异步方法始终返回
Promise<T>,不要直接返回T。 - 避免使用
any。 如果确实无法为某项内容标注类型,请使用unknown,并通过类型守卫进行缩窄。
参考模式
加载 [[assets/type-patterns.ts|assets/type-patterns.ts]],以查看属性类型、方法类型和事件处理程序类型的内联示例。
步骤 4 — 生成 .d.ts
在 .ts 文件旁创建 componentName.d.ts。文件必须:
- 仅包含
@api成员——不能包含私有状态、内部方法,除非生命周期钩子本身带有@api,否则也不能包含。 - 将
@apiJSDoc 原样保留(包括@type、@required、@default、@param、@returns标签),并直接放在每个声明上方。 - 声明 LWC 模块命名空间
c/componentName(如果组织使用不同的命名空间,则使用该命名空间)。
模板:将 [[assets/dts-template.ts|assets/dts-template.ts]] 加载为起始 .d.ts 结构。
如果组件没有 @api 成员,仍需生成模块声明,并添加注释说明没有公开接口——不要跳过该文件。
步骤 5 — 编译和测试
- 运行 TypeScript 编译器(
tsc --noEmit或构建中的等效命令)。完成前解决所有错误;不要使用@ts-ignore补丁。 - 运行组件现有的 Jest 测试。行为应保持一致。
- 无条件运行随附的消费者查找脚本——输出为空是有效结果,不能作为跳过的理由。脚本会从
sfdx-project.json的packageDirectories解析搜索路径(若不存在则回退到<project-root>),拒绝任何超出项目根目录的条目,并在内部执行 LWC 导入搜索,因此调用结果完全确定:
"<skill_dir>/scripts/find-consumers.sh" "<project-root>" "<componentName>"
对于每个匹配项,确认消费者所需的类型仍与新的 .d.ts 公开接口一致。
步骤 6 — 预期的最终打包结构
componentName/
├── componentName.ts # Main TypeScript implementation
├── componentName.html # Template (unchanged)
├── componentName.css # Styles (unchanged)
└── componentName.d.ts # Type definitions (new)
验证清单
转换前:
- 组件是有效的 JS,且所有测试均通过。
- 已识别每个
@api成员及其预期类型。
转换后:
- 使用了
git mv,以保留历史记录。 -
.ts中的每个变量和参数都有明确类型(没有隐式any)。 - 复杂对象结构应放在
interface/type别名中,而不是重复内联编写。 - 仅在字段确实可选时使用
?。 -
.d.ts存在,声明了c/componentName,继承LightningElement,且仅包含@api成员。 -
.d.ts中原样保留了每个@apiJSDoc。 -
tsc通过且没有错误;没有使用@ts-ignore,也没有将any用作变通方案。 - Jest 测试仍然通过。
常见陷阱
- 使用
any来屏蔽错误。 应解决实际的类型问题。如果值确实未知,请使用unknown和类型守卫。 - 在
.d.ts中包含私有成员。.d.ts是公开契约。内部生命周期钩子和辅助方法不得泄漏。 - 重命名时丢失 JSDoc。 重命名前后都要检查——
.ts和.d.ts中都必须包含@api成员的 JSDoc 注释。 - 跳过
git mv。 这会让审查变得困难,也会干扰 blame。 - 忘记异步返回类型。 带有
async关键字的foo()总是返回Promise。请声明这一点。 - 将
onclick声明为PointerEvent。click是MouseEvent(包括键盘触发的点击),因此这类事件的PointerEvent字段(如pointerType)未定义。将onclick声明为MouseEvent;PointerEvent仅用于onpointer*处理程序。只有当代码明确区分处理TouchEvent时,才使用MouseEvent | TouchEvent。