experience-lwc-typescript-migrate
forcedotcom/sf-skills
Convert existing Lightning Web Components from JavaScript to TypeScript with full type annotations and public API definitions.
What is experience-lwc-typescript-migrate?
This skill migrates a single LWC or folder of components from .js to .ts, adding complete type annotations and generating a .d.ts file that exposes only the component's @api surface. Use it when you need to add TypeScript types to an existing, working JavaScript LWC.
- Rename .js files to .ts using git mv to preserve history
- Add type annotations to all properties, methods, and event handlers with priority on @api members
- Generate a .d.ts declaration file that exposes only the public @api surface
- Create interface and type aliases for complex object shapes
- Validate JSDoc claims against actual code usage and convert to TypeScript syntax
- Compile with tsc and verify existing Jest tests still pass
How to install experience-lwc-typescript-migrate
npx skills add https://github.com/forcedotcom/sf-skills --skill experience-lwc-typescript-migrate- Component must build and run correctly in JavaScript today
- git (≥2.0.0) must be available for renaming files
- TypeScript compiler (tsc ≥4.0.0) wired into the build pipeline
- jq (≥1.6) for JSON processing in build scripts
How to use experience-lwc-typescript-migrate
- 1.Read the component bundle and identify all @api members and their types
- 2.Run git mv to rename componentName.js to componentName.ts (preserves history)
- 3.Add type annotations in priority order: @api members first, then complex shapes as interfaces, then internal state
- 4.Generate componentName.d.ts next to the .ts file with only @api members and their JSDoc preserved
- 5.Run tsc --noEmit to compile and resolve all type errors without @ts-ignore workarounds
- 6.Run existing Jest tests to confirm behavior is identical
- 7.Execute the bundled find-consumers.sh script to locate and verify all consuming components
Use cases
- Migrate a single LWC component from JavaScript to TypeScript with proper typing
- Add a .d.ts file to an existing LWC so other components can import it safely with type checking
- Upgrade JSDoc-style type hints to real TypeScript types in a working component
- Type-annotate @api properties and methods for better IDE support and consumer safety
- Convert a folder of related LWC components to TypeScript in bulk
- Salesforce developers maintaining Lightning Web Components
- Teams adopting TypeScript in existing LWC codebases
- Developers creating reusable LWC libraries that need public type definitions
- Organizations improving type safety in their component ecosystems
experience-lwc-typescript-migrate FAQ
Still produce the .d.ts file with a module declaration and a comment explaining there is no public surface; do not skip the file.
Always use git mv — plain mv loses the history link that TypeScript reviewers and blame tools rely on.
Use MouseEvent, not PointerEvent, because click is dispatched as MouseEvent even for keyboard-activated clicks; PointerEvent fields like pointerType would be undefined in those cases.
No; solve the actual type instead. If the value is truly unknown, use unknown with a type guard rather than any or @ts-ignore.
Yes, type them in the .ts file, but do not export or include them in the .d.ts — the .d.ts is the public contract only.
Full instructions (SKILL.md)
Source of truth, from forcedotcom/sf-skills.
name: experience-lwc-typescript-migrate
description: "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."
metadata:
version: "1.0"
domains: ["Experience"]
relatedSkills:
- "experience-lwc-generate"
cliTools:
- tool: ["git"]
semver: ">=2.0.0"
- tool: ["jq"]
semver: ">=1.6"
- tool: ["tsc"]
semver: ">=4.0.0"
<!-- adk-managed-skill -->
Converting LWC to 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).
When to Use This Skill
- User wants to migrate a single component or a folder of components from
.jsto.ts. - User needs a
.d.tsfor an existing LWC so other components (or an external TypeScript host) can import it safely. - User is adding type annotations to an already-renamed
.tsLWC that hasn't been properly typed yet. - User wants JSDoc-style type hints upgraded to real TypeScript types.
Prerequisites
- The component builds and runs correctly in JavaScript today.
gitis available (the rename must preserve history viagit mv).- A TypeScript compiler is wired into the build (either the SFDX TS
pipeline or a standalone
tscstep).
Workflow
Step 1 — Read the component
Open every file in the bundle:
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
@apidecorator? - 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?
Step 2 — Rename .js → .ts using git mv
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.
Step 3 — Add type annotations in the .ts
Apply types in this priority order so you stop as soon as the public contract is solid:
@apiproperties 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.- Complex shapes become
interfaceortypealiases — not inline shapes repeated everywhere. - Optional members use
?only when the value is genuinely allowed to beundefined. Do not sprinkle?defensively. - Private/internal state — still type it, but don't export the
types. Use
privatefor members that must never be touched by consumers. - Event handlers — prefer precise DOM event types:
MouseEventforonclick(and other click-like handlers).clickis dispatched as aMouseEvent— including keyboard-activated clicks — so typing it asPointerEventwould let handlers rely on pointer-only fields (pointerType,pressure, etc.) that are undefined in those cases.PointerEventforonpointerdown/onpointerup/onpointermoveand otherpointer*handlers where pointer-specific fields are actually meaningful.CustomEvent<{ detail: ... }>for LWC custom events.Eventis the last resort; document why when using it.
- Async methods always return
Promise<T>— never bareT. - Avoid
any. If you genuinely can't type something, useunknownand narrow with a type guard.
Reference patterns
Load [[assets/type-patterns.ts|assets/type-patterns.ts]] as an inline example covering property types, method types, and event handler types.
Step 4 — Generate the .d.ts
Create componentName.d.ts next to the .ts. It must:
- Contain only
@apimembers — no private state, no internal methods, no lifecycle hooks unless they are themselves@api. - Preserve
@apiJSDoc verbatim (including@type,@required,@default,@param,@returnstags) 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.
Step 5 — Compile and test
- Run the TypeScript compiler (
tsc --noEmitor the build's equivalent). Resolve every error before calling it done; no@ts-ignorepatches. - 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'spackageDirectories(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:
"<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.
Step 6 — Expected final bundle shape
componentName/
├── componentName.ts # Main TypeScript implementation
├── componentName.html # Template (unchanged)
├── componentName.css # Styles (unchanged)
└── componentName.d.ts # Type definitions (new)
Verification Checklist
Before conversion:
- Component is valid JS and all tests pass.
- You've identified every
@apimember and its intended type.
After conversion:
-
git mvwas used so history is preserved. - Every variable and parameter in the
.tshas a concrete type (no implicitany). - Complex object shapes live in
interface/typealiases, not inline repeats. - Optional
?is only on genuinely optional fields. -
.d.tsexists, declaresc/componentName, extendsLightningElement, includes only@apimembers. - Every
@apiJSDoc is preserved verbatim in the.d.ts. -
tscpasses with zero errors; no@ts-ignoreoranyused as a workaround. - Jest tests still pass.
Common Pitfalls
- Using
anyto silence errors. Solve the actual type instead. If the value is truly unknown, useunknown+ a type guard. - Including private members in the
.d.ts. The.d.tsis the public contract. Internal lifecycle and helpers must not leak. - Losing JSDoc during the rename. Scan before and after — JSDoc
comments on
@apimembers must appear in both the.tsand.d.ts. - Skipping
git mv. Makes review miserable and confuses blame. - Forgetting async return types.
foo()with anasynckeyword always returns aPromise. Declare it. - Typing
onclickasPointerEvent.clickis aMouseEvent(keyboard-triggered clicks included), soPointerEventfields likepointerTypeare undefined for those events. TypeonclickasMouseEvent; reservePointerEventforonpointer*handlers. UseMouseEvent | TouchEventonly when the code branches onTouchEventdistinctly.
Support Resources
Related skills
More from forcedotcom/sf-skills and the wider catalog.

experience-lwr-site-generate
Create and manage Salesforce Experience Cloud LWR sites with metadata-driven configuration.

experience-portal-create
Create new Digital Experience portals and communities in Salesforce with MIAW integration.

experience-ui-bundle-2gp-deploy
Package and distribute Salesforce UI Bundles as second-generation managed or unlocked packages across orgs.

experience-ui-bundle-agentforce-client-generate
Add, configure, and manage Agentforce Conversation Client in React or Angular UI Bundle projects.

experience-ui-bundle-app-coordinate
Orchestrate end-to-end React UI bundle app builds on Salesforce by coordinating specialized skills in dependency order.

experience-ui-bundle-custom-app-generate
Create Custom Applications to host React UI bundles in Lightning Experience without Digital Experience Sites.