npx skills add ...
npx skills add forcedotcom/sf-skills --skill experience-lwc-typescript-migrate
Use when converting an existing JavaScript Lightning Web Component (.js, .html, .css) to TypeScript with full type annotations and a matching `.d.ts` file that exposes only the component's `@api` surface. TRIGGER when the user says \"convert LWC to TypeScript\", \"migrate LWC to TS\", \"rename .js to .ts for this component\", \"add types to my LWC\", \"generate .d.ts for this LWC\", \"type-annotate @api properties\", or \"produce declare module 'c/componentName' definitions\". DO NOT TRIGGER when the user is authoring a brand-new LWC from scratch (use experience-lwc-generate), generating Jest tests for an existing LWC (use experience-lwc-generate), or migrating an Aura component to LWC.
npx skills add forcedotcom/sf-skills --skill experience-lwc-typescript-migrate
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).
.js to .ts..d.ts for an existing LWC so other components (or an
external TypeScript host) can import it safely..ts LWC that
hasn't been properly typed yet.git is available (the rename must preserve history via git mv).tsc step).Open every file in the bundle:
Understand:
LightningElement? What is the class name?@api decorator?.js → .ts using git mvRepeat 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.
.tsApply types in this priority order so you stop as soon as the public contract is solid:
@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.interface or type aliases — not inline
shapes repeated everywhere.? only when the value is genuinely allowed
to be undefined. Do not sprinkle ? defensively.private for members that must never be touched by
consumers.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.Promise<T> — never bare T.any. If you genuinely can't type something, use unknown
and narrow with a type guard.Load [[assets/type-patterns.ts|assets/type-patterns.ts]] as an inline example covering property types, method types, and event handler types.
.d.tsCreate componentName.d.ts next to the .ts. It must:
@api members — no private state, no internal
methods, no lifecycle hooks unless they are themselves @api.@api JSDoc verbatim (including @type, @required,
@default, @param, @returns tags) directly above each declaration.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.
tsc --noEmit or the build's equivalent).
Resolve every error before calling it done; no @ts-ignore patches.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:For each match, confirm the consumer's expected types still align with
the new .d.ts public surface.
Before conversion:
@api member and its intended type.After conversion:
git mv was used so history is preserved..ts has a concrete type
(no implicit any).interface / type aliases, not
inline repeats.? is only on genuinely optional fields..d.ts exists, declares c/componentName, extends
LightningElement, includes only @api members.@api JSDoc is preserved verbatim in the .d.ts.tsc passes with zero errors; no @ts-ignore or any used as a
workaround.any to silence errors. Solve the actual type instead.
If the value is truly unknown, use unknown + a type guard..d.ts. The .d.ts is the
public contract. Internal lifecycle and helpers must not leak.@api members must appear in both the .ts and .d.ts.git mv. Makes review miserable and confuses blame.foo() with an async keyword
always returns a Promise. Declare it.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.componentName/
├── componentName.ts # Main TypeScript implementation
├── componentName.html # Template (unchanged)
├── componentName.css # Styles (unchanged)
└── componentName.d.ts # Type definitions (new)