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
.ts
implementation plus a
.d.ts
file that only exposes
@api
members (the public surface other LWCs consume).
将Lightning Web Component包从JavaScript转换为TypeScript。交付成果是一个完全类型化的
.ts
实现加上一个仅暴露
@api
成员的
.d.ts
文件(其他LWC会调用的公共接口)。

When to Use This Skill

何时使用此技能

  • User wants to migrate a single component or a folder of components from
    .js
    to
    .ts
    .
  • User needs a
    .d.ts
    for an existing LWC so other components (or an external TypeScript host) can import it safely.
  • User is adding type annotations to an already-renamed
    .ts
    LWC that hasn't been properly typed yet.
  • User wants JSDoc-style type hints upgraded to real TypeScript types.
  • 用户希望将单个组件或一个文件夹中的组件从
    .js
    迁移到
    .ts
  • 用户需要为现有LWC生成
    .d.ts
    文件,以便其他组件(或外部TypeScript宿主)可以安全地导入它。
  • 用户正在为已重命名为
    .ts
    但尚未正确添加类型的LWC添加类型注解。
  • 用户希望将JSDoc风格的类型提示升级为真正的TypeScript类型。

Prerequisites

前提条件

  • The component builds and runs correctly in JavaScript today.
  • git
    is available (the rename must preserve history via
    git mv
    ).
  • A TypeScript compiler is wired into the build (either the SFDX TS pipeline or a standalone
    tsc
    step).

  • 该组件目前可以用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.ts
Understand:
  • What extends
    LightningElement
    ? What is the class name?
  • Which fields and methods carry the
    @api
    decorator?
  • 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

步骤2 — 使用
git mv
.js
重命名为
.ts

bash
git mv componentName/componentName.js componentName/componentName.ts
Repeat for any helper
.js
files in the bundle (unless they're already
.ts
). Never plain
mv
— that loses the history link TypeScript reviewers rely on.
bash
git mv componentName/componentName.js componentName/componentName.ts
对包中的所有辅助
.js
文件重复此操作(除非它们已经是
.ts
文件)。绝对不要使用普通的
mv
命令——这会丢失TypeScript评审人员依赖的历史关联。

Step 3 — Add type annotations in the
.ts

步骤3 — 在
.ts
文件中添加类型注解

Apply types in this priority order so you stop as soon as the public contract is solid:
  1. @api
    properties and methods first.
    Generate JSDoc if it's missing, then translate JSDoc types to TS syntax (
    string
    ,
    number
    ,
    boolean
    ,
    Promise<T>
    ). Validate each JSDoc claim against the code before trusting it.
  2. Complex shapes become
    interface
    or
    type
    aliases
    — not inline shapes repeated everywhere.
  3. Optional members use
    ?
    only when the value is genuinely allowed to be
    undefined
    . Do not sprinkle
    ?
    defensively.
  4. Private/internal state — still type it, but don't export the types. Use
    private
    for members that must never be touched by consumers.
  5. Event handlers — prefer precise DOM event types:
    • MouseEvent
      for
      onclick
      (and other click-like handlers).
      click
      is dispatched as a
      MouseEvent
      — including keyboard-activated clicks — so typing it as
      PointerEvent
      would let handlers rely on pointer-only fields (
      pointerType
      ,
      pressure
      , etc.) that are undefined in those cases.
    • PointerEvent
      for
      onpointerdown
      /
      onpointerup
      /
      onpointermove
      and other
      pointer*
      handlers where pointer-specific fields are actually meaningful.
    • CustomEvent<{ detail: ... }>
      for LWC custom events.
    • Event
      is the last resort; document why when using it.
  6. Async methods always return
    Promise<T>
    — never bare
    T
    .
  7. Avoid
    any
    .
    If you genuinely can't type something, use
    unknown
    and narrow with a type guard.
按照以下优先级顺序添加类型,确保公共契约稳定后再停止:
  1. 优先处理
    @api
    属性和方法
    。如果缺少JSDoc则生成,然后将JSDoc类型转换为TS语法(
    string
    number
    boolean
    Promise<T>
    )。在信任JSDoc之前,需根据代码验证每个JSDoc声明的准确性。
  2. 复杂结构转为
    interface
    type
    别名
    ——不要在多个地方重复内联结构。
  3. 可选成员仅在值确实允许为
    undefined
    时使用
    ?
    。不要随意添加
    ?
    作为防御性措施。
  4. 私有/内部状态——仍需添加类型,但不要导出这些类型。对绝对不能被消费者访问的成员使用
    private
    修饰符。
  5. 事件处理程序——优先使用精确的DOM事件类型:
    • onclick
      (及类似点击处理程序)使用
      MouseEvent
      click
      事件以
      MouseEvent
      形式触发——包括键盘激活的点击——因此如果将其类型设为
      PointerEvent
      ,处理程序可能会依赖仅指针事件才有的字段(
      pointerType
      pressure
      等),而这些字段在键盘触发的点击中是未定义的。
    • onpointerdown
      /
      onpointerup
      /
      onpointermove
      及其他
      pointer*
      处理程序使用
      PointerEvent
      ,这些场景中指针特定字段才真正有用。
    • LWC自定义事件使用
      CustomEvent<{ detail: ... }>
    • Event
      是最后选择;使用时需说明原因。
  6. 异步方法始终返回
    Promise<T>
    ——绝不要返回裸类型
    T
  7. 避免使用
    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

步骤4 — 生成
.d.ts
文件

Create
componentName.d.ts
next to the
.ts
. It must:
  • Contain only
    @api
    members
    — no private state, no internal methods, no lifecycle hooks unless they are themselves
    @api
    .
  • Preserve
    @api
    JSDoc verbatim (including
    @type
    ,
    @required
    ,
    @default
    ,
    @param
    ,
    @returns
    tags) directly above each declaration.
  • Declare the LWC module namespace
    c/componentName
    (or the org's namespace if different).
Template: load [[assets/dts-template.ts|assets/dts-template.ts]] as the starting
.d.ts
shape.
If the component has no
@api
members, still produce the module declaration with a comment explaining there's no public surface — don't skip the file.
.ts
文件旁边创建
componentName.d.ts
。该文件必须:
  • 仅包含
    @api
    成员
    ——不包含私有状态、内部方法、生命周期钩子(除非它们本身带有
    @api
    装饰器)。
  • 原封不动保留
    @api
    的JSDoc(包括
    @type
    @required
    @default
    @param
    @returns
    标签),直接放在每个声明上方。
  • 声明LWC模块命名空间
    c/componentName
    (如果是组织自定义命名空间则使用对应命名空间)。
模板:加载[[assets/dts-template.ts|assets/dts-template.ts]]作为
.d.ts
文件的初始结构。
如果组件没有
@api
成员,仍需生成模块声明并添加注释说明没有公共接口——不要跳过此文件。

Step 5 — Compile and test

步骤5 — 编译和测试

  • Run the TypeScript compiler (
    tsc --noEmit
    or the build's equivalent). Resolve every error before calling it done; no
    @ts-ignore
    patches.
  • 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
    sfdx-project.json
    's
    packageDirectories
    (or falls back to
    <project-root>
    ), rejects any entry that escapes the project root, and performs the LWC-import search internally so the invocation is fully deterministic:
bash
"<skill_dir>/scripts/find-consumers.sh" "<project-root>" "<componentName>"
For each match, confirm the consumer's expected types still align with the new
.d.ts
public surface.
  • 运行TypeScript编译器(
    tsc --noEmit
    或构建流程中的等效命令)。在完成之前解决所有错误;不要使用
    @ts-ignore
    补丁。
  • 运行组件现有的Jest测试。行为应与转换前完全一致。
  • 无条件运行捆绑的consumer-finder脚本——空输出是有效结果,不是跳过的理由。该脚本从
    sfdx-project.json
    packageDirectories
    解析搜索路径(如果没有则回退到
    <project-root>
    ),拒绝任何超出项目根目录的条目,并在内部执行LWC导入搜索,因此调用结果完全确定:
bash
"<skill_dir>/scripts/find-consumers.sh" "<project-root>" "<componentName>"
对于每个匹配项,确认消费者的预期类型仍与新的
.d.ts
公共接口一致。

Step 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
    @api
    member and its intended type.
After conversion:
  • git mv
    was used so history is preserved.
  • Every variable and parameter in the
    .ts
    has a concrete type (no implicit
    any
    ).
  • Complex object shapes live in
    interface
    /
    type
    aliases, not inline repeats.
  • Optional
    ?
    is only on genuinely optional fields.
  • .d.ts
    exists, declares
    c/componentName
    , extends
    LightningElement
    , includes only
    @api
    members.
  • Every
    @api
    JSDoc is preserved verbatim in the
    .d.ts
    .
  • tsc
    passes with zero errors; no
    @ts-ignore
    or
    any
    used as a workaround.
  • Jest tests still pass.

转换前:
  • 组件是有效的JS,且所有测试通过。
  • 已识别所有
    @api
    成员及其预期类型。
转换后:
  • 使用了
    git mv
    以保留历史记录。
  • .ts
    文件中的每个变量和参数都有具体类型(无隐式
    any
    )。
  • 复杂对象结构定义在
    interface
    /
    type
    别名中,而非重复的内联结构。
  • 可选
    ?
    仅用于真正可选的字段。
  • .d.ts
    文件已存在,声明了
    c/componentName
    ,继承了
    LightningElement
    ,且包含
    @api
    成员。
  • 所有
    @api
    的JSDoc都原封不动保留在
    .d.ts
    文件中。
  • tsc
    运行无错误;未使用
    @ts-ignore
    any
    作为临时解决方案。
  • Jest测试仍能通过。

Common Pitfalls

常见陷阱

  • Using
    any
    to silence errors.
    Solve the actual type instead. If the value is truly unknown, use
    unknown
    + a type guard.
  • Including private members in the
    .d.ts
    .
    The
    .d.ts
    is the public contract. Internal lifecycle and helpers must not leak.
  • Losing JSDoc during the rename. Scan before and after — JSDoc comments on
    @api
    members must appear in both the
    .ts
    and
    .d.ts
    .
  • Skipping
    git mv
    .
    Makes review miserable and confuses blame.
  • Forgetting async return types.
    foo()
    with an
    async
    keyword always returns a
    Promise
    . Declare it.
  • Typing
    onclick
    as
    PointerEvent
    .
    click
    is a
    MouseEvent
    (keyboard-triggered clicks included), so
    PointerEvent
    fields like
    pointerType
    are undefined for those events. Type
    onclick
    as
    MouseEvent
    ; reserve
    PointerEvent
    for
    onpointer*
    handlers. Use
    MouseEvent | TouchEvent
    only when the code branches on
    TouchEvent
    distinctly.
  • 使用
    any
    来消除错误
    。应解决实际的类型问题。如果值确实未知,请使用
    unknown
    +类型守卫。
  • .d.ts
    中包含私有成员
    .d.ts
    是公共契约。内部生命周期方法和辅助方法绝对不能泄露。
  • 重命名过程中丢失JSDoc。转换前后都要检查——
    @api
    成员的JSDoc注释必须同时出现在
    .ts
    .d.ts
    文件中。
  • 跳过
    git mv
    。这会让代码评审变得困难,也会混淆代码 blame 信息。
  • 忘记异步返回类型。带有
    async
    关键字的
    foo()
    始终返回
    Promise
    。请声明此类型。
  • onclick
    类型设为
    PointerEvent
    click
    MouseEvent
    (包括键盘触发的点击),因此
    PointerEvent
    的字段如
    pointerType
    在这些事件中是未定义的。将
    onclick
    类型设为
    MouseEvent
    ;仅在
    onpointer*
    处理程序中使用
    PointerEvent
    。只有当代码明确区分
    TouchEvent
    时,才使用
    MouseEvent | TouchEvent

Support Resources

支持资源