experience-lwc-typescript-migrate
Compare original and translation side by side
🇺🇸
Original
English🇨🇳
Translation
Chinese<!-- adk-managed-skill -->
<!-- adk-managed-skill -->
Converting LWC to TypeScript
将LWC转换为TypeScript
Convert a Lightning Web Component bundle from JavaScript to TypeScript. The
deliverable is a fully-typed implementation plus a file
that only exposes members (the public surface other LWCs consume).
.ts.d.ts@api将Lightning Web Component包从JavaScript转换为TypeScript。交付成果是一个完全类型化的实现加上一个仅暴露成员的文件(其他LWC会调用的公共接口)。
.ts@api.d.tsWhen to Use This Skill
何时使用此技能
- User wants to migrate a single component or a folder of components from
to
.js..ts - User needs a for an existing LWC so other components (or an external TypeScript host) can import it safely.
.d.ts - User is adding type annotations to an already-renamed LWC that hasn't been properly typed yet.
.ts - User wants JSDoc-style type hints upgraded to real TypeScript types.
- 用户希望将单个组件或一个文件夹中的组件从迁移到
.js。.ts - 用户需要为现有LWC生成文件,以便其他组件(或外部TypeScript宿主)可以安全地导入它。
.d.ts - 用户正在为已重命名为但尚未正确添加类型的LWC添加类型注解。
.ts - 用户希望将JSDoc风格的类型提示升级为真正的TypeScript类型。
Prerequisites
前提条件
- The component builds and runs correctly in JavaScript today.
- is available (the rename must preserve history via
git).git mv - A TypeScript compiler is wired into the build (either the SFDX TS
pipeline or a standalone step).
tsc
- 该组件目前可以用JavaScript正常构建和运行。
- 已安装(必须通过
git重命名以保留历史记录)。git mv - 构建流程中已接入TypeScript编译器(SFDX TS流水线或独立的步骤均可)。
tsc
Workflow
工作流程
Step 1 — Read the component
步骤1 — 读取组件
Open every file in the bundle:
text
componentName/
├── componentName.js
├── componentName.html
├── componentName.css
└── (possibly) __tests__/, __utam__/, existing .d.tsUnderstand:
- What extends ? What is the class name?
LightningElement - Which fields and methods carry the decorator?
@api - Which properties/methods have existing JSDoc (use as a type hint starting point, but validate against actual usage — JSDoc lies).
- Which parameters / return types can you infer from how the code is called internally?
打开包中的所有文件:
text
componentName/
├── componentName.js
├── componentName.html
├── componentName.css
└── (可能包含) __tests__/, __utam__/, 已有的.d.ts了解以下内容:
- 哪个类继承了?类名是什么?
LightningElement - 哪些字段和方法带有装饰器?
@api - 哪些属性/方法已有JSDoc(可作为类型提示的起点,但需根据实际使用情况验证——JSDoc可能不准确)。
- 可以从代码内部的调用方式推断出哪些参数/返回类型?
Step 2 — Rename .js
→ .ts
using git mv
.js.tsgit mv步骤2 — 使用git mv
将.js
重命名为.ts
git mv.js.tsbash
git mv componentName/componentName.js componentName/componentName.tsRepeat for any helper files in the bundle (unless they're already
). Never plain — that loses the history link TypeScript
reviewers rely on.
.js.tsmvbash
git mv componentName/componentName.js componentName/componentName.ts对包中的所有辅助文件重复此操作(除非它们已经是文件)。绝对不要使用普通的命令——这会丢失TypeScript评审人员依赖的历史关联。
.js.tsmvStep 3 — Add type annotations in the .ts
.ts步骤3 — 在.ts
文件中添加类型注解
.tsApply types in this priority order so you stop as soon as the public
contract is solid:
- properties and methods first. Generate JSDoc if it's missing, then translate JSDoc types to TS syntax (
@api,string,number,boolean). Validate each JSDoc claim against the code before trusting it.Promise<T> - Complex shapes become or
interfacealiases — not inline shapes repeated everywhere.type - Optional members use only when the value is genuinely allowed to be
?. Do not sprinkleundefineddefensively.? - Private/internal state — still type it, but don't export the
types. Use for members that must never be touched by consumers.
private - Event handlers — prefer precise DOM event types:
- for
MouseEvent(and other click-like handlers).onclickis dispatched as aclick— including keyboard-activated clicks — so typing it asMouseEventwould let handlers rely on pointer-only fields (PointerEvent,pointerType, etc.) that are undefined in those cases.pressure - for
PointerEvent/onpointerdown/onpointerupand otheronpointermovehandlers where pointer-specific fields are actually meaningful.pointer* - for LWC custom events.
CustomEvent<{ detail: ... }> - is the last resort; document why when using it.
Event
- Async methods always return — never bare
Promise<T>.T - Avoid . If you genuinely can't type something, use
anyand narrow with a type guard.unknown
按照以下优先级顺序添加类型,确保公共契约稳定后再停止:
- 优先处理属性和方法。如果缺少JSDoc则生成,然后将JSDoc类型转换为TS语法(
@api、string、number、boolean)。在信任JSDoc之前,需根据代码验证每个JSDoc声明的准确性。Promise<T> - 复杂结构转为或
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
Reference patterns
参考模式
Load [[assets/type-patterns.ts|assets/type-patterns.ts]] as an inline example
covering property types, method types, and event handler types.
加载[[assets/type-patterns.ts|assets/type-patterns.ts]]作为内联示例,涵盖属性类型、方法类型和事件处理程序类型。
Step 4 — Generate the .d.ts
.d.ts步骤4 — 生成.d.ts
文件
.d.tsCreate next to the . It must:
componentName.d.ts.ts- Contain only members — no private state, no internal methods, no lifecycle hooks unless they are themselves
@api.@api - Preserve JSDoc verbatim (including
@api,@type,@required,@default,@paramtags) directly above each declaration.@returns - Declare the LWC module namespace (or the org's namespace if different).
c/componentName
Template: load [[assets/dts-template.ts|assets/dts-template.ts]] as the
starting shape.
.d.tsIf the component has no members, still produce the module
declaration with a comment explaining there's no public surface — don't
skip the file.
@api在文件旁边创建。该文件必须:
.tscomponentName.d.ts- 仅包含成员——不包含私有状态、内部方法、生命周期钩子(除非它们本身带有
@api装饰器)。@api - 原封不动保留的JSDoc(包括
@api、@type、@required、@default、@param标签),直接放在每个声明上方。@returns - 声明LWC模块命名空间(如果是组织自定义命名空间则使用对应命名空间)。
c/componentName
模板:加载[[assets/dts-template.ts|assets/dts-template.ts]]作为文件的初始结构。
.d.ts如果组件没有成员,仍需生成模块声明并添加注释说明没有公共接口——不要跳过此文件。
@apiStep 5 — Compile and test
步骤5 — 编译和测试
- Run the TypeScript compiler (or the build's equivalent). Resolve every error before calling it done; no
tsc --noEmitpatches.@ts-ignore - Run the component's existing Jest tests. The behavior should be identical.
- Run the bundled consumer-finder unconditionally — empty output is a
valid result, not a reason to skip. The script resolves the search
paths from 's
sfdx-project.json(or falls back topackageDirectories), rejects any entry that escapes the project root, and performs the LWC-import search internally so the invocation is fully deterministic:<project-root>
bash
"<skill_dir>/scripts/find-consumers.sh" "<project-root>" "<componentName>"For each match, confirm the consumer's expected types still align with
the new public surface.
.d.ts- 运行TypeScript编译器(或构建流程中的等效命令)。在完成之前解决所有错误;不要使用
tsc --noEmit补丁。@ts-ignore - 运行组件现有的Jest测试。行为应与转换前完全一致。
- 无条件运行捆绑的consumer-finder脚本——空输出是有效结果,不是跳过的理由。该脚本从的
sfdx-project.json解析搜索路径(如果没有则回退到packageDirectories),拒绝任何超出项目根目录的条目,并在内部执行LWC导入搜索,因此调用结果完全确定:<project-root>
bash
"<skill_dir>/scripts/find-consumers.sh" "<project-root>" "<componentName>"对于每个匹配项,确认消费者的预期类型仍与新的公共接口一致。
.d.tsStep 6 — Expected final bundle shape
步骤6 — 预期的最终包结构
text
componentName/
├── componentName.ts # Main TypeScript implementation
├── componentName.html # Template (unchanged)
├── componentName.css # Styles (unchanged)
└── componentName.d.ts # Type definitions (new)text
componentName/
├── componentName.ts # 主TypeScript实现
├── componentName.html # 模板(未修改)
├── componentName.css # 样式(未修改)
└── componentName.d.ts # 类型定义(新增)Verification Checklist
验证检查清单
Before conversion:
- Component is valid JS and all tests pass.
- You've identified every member and its intended type.
@api
After conversion:
- was used so history is preserved.
git mv - Every variable and parameter in the has a concrete type (no implicit
.ts).any - Complex object shapes live in /
interfacealiases, not inline repeats.type - Optional is only on genuinely optional fields.
? - exists, declares
.d.ts, extendsc/componentName, includes onlyLightningElementmembers.@api - Every JSDoc is preserved verbatim in the
@api..d.ts - passes with zero errors; no
tscor@ts-ignoreused as a workaround.any - Jest tests still pass.
转换前:
- 组件是有效的JS,且所有测试通过。
- 已识别所有成员及其预期类型。
@api
转换后:
- 使用了以保留历史记录。
git mv - 文件中的每个变量和参数都有具体类型(无隐式
.ts)。any - 复杂对象结构定义在/
interface别名中,而非重复的内联结构。type - 可选仅用于真正可选的字段。
? - 文件已存在,声明了
.d.ts,继承了c/componentName,且仅包含LightningElement成员。@api - 所有的JSDoc都原封不动保留在
@api文件中。.d.ts - 运行无错误;未使用
tsc或@ts-ignore作为临时解决方案。any - Jest测试仍能通过。
Common Pitfalls
常见陷阱
- Using to silence errors. Solve the actual type instead. If the value is truly unknown, use
any+ a type guard.unknown - Including private members in the . The
.d.tsis the public contract. Internal lifecycle and helpers must not leak..d.ts - Losing JSDoc during the rename. Scan before and after — JSDoc
comments on members must appear in both the
@apiand.ts..d.ts - Skipping . Makes review miserable and confuses blame.
git mv - Forgetting async return types. with an
foo()keyword always returns aasync. Declare it.Promise - Typing as
onclick.PointerEventis aclick(keyboard-triggered clicks included), soMouseEventfields likePointerEventare undefined for those events. TypepointerTypeasonclick; reserveMouseEventforPointerEventhandlers. Useonpointer*only when the code branches onMouseEvent | TouchEventdistinctly.TouchEvent
- 使用来消除错误。应解决实际的类型问题。如果值确实未知,请使用
any+类型守卫。unknown - 在中包含私有成员。
.d.ts是公共契约。内部生命周期方法和辅助方法绝对不能泄露。.d.ts - 重命名过程中丢失JSDoc。转换前后都要检查——成员的JSDoc注释必须同时出现在
@api和.ts文件中。.d.ts - 跳过。这会让代码评审变得困难,也会混淆代码 blame 信息。
git mv - 忘记异步返回类型。带有关键字的
async始终返回foo()。请声明此类型。Promise - 将类型设为
onclick。PointerEvent是click(包括键盘触发的点击),因此MouseEvent的字段如PointerEvent在这些事件中是未定义的。将pointerType类型设为onclick;仅在MouseEvent处理程序中使用onpointer*。只有当代码明确区分PointerEvent时,才使用TouchEvent。MouseEvent | TouchEvent