
LWC TypeScript Migration
OfficialFreeSeamlessly convert Lightning Web Components to TypeScript.
Free · Opens the source repo
What LWC TypeScript Migration does
The LWC TypeScript Migration skill is designed to assist developers in converting existing JavaScript Lightning Web Components (LWCs) into TypeScript. This process not only involves renaming files from .js to .ts but also includes adding full type annotations and generating a corresponding .d.ts file that exposes only the @api surface of the component. This ensures that other components or external TypeScript hosts can safely import the component with the correct types defined.
To effectively use this skill, users should have a working JavaScript component that builds and runs correctly. The skill guides users through a structured workflow that begins with analyzing the component files, identifying @api members, and then applying type annotations in a prioritized manner. The final output is a fully typed TypeScript implementation alongside a .d.ts file, which is crucial for maintaining type safety and clarity in larger applications.
This skill is particularly beneficial for developers looking to modernize their codebase by migrating to TypeScript, which offers enhanced type safety and better tooling support. By following the provided steps, users can ensure that their components are not only converted but also maintain their functionality and compatibility with existing consumers.
However, this skill is not intended for creating new LWCs from scratch or for generating Jest tests for existing components. It is specifically focused on the migration process, making it an essential tool for developers who are looking to upgrade their existing JavaScript components to TypeScript without losing any history or functionality.
When to use it
Use this skill when you need to migrate existing LWC components from JavaScript to TypeScript, particularly when adding type annotations and generating `.d.ts` files.
When not to use it
This skill is not suitable for creating new LWC components or for generating tests; it is focused solely on migration from JavaScript to TypeScript.
What you can build with it
Migrating a Single Component
Use this skill to convert a single LWC from JavaScript to TypeScript, ensuring type safety.
Batch Migration of Multiple Components
Apply this skill to a folder of components to streamline the migration process to TypeScript.
Updating Type Annotations
Leverage this skill to enhance existing TypeScript files with proper type annotations and generate .d.ts files.
How to install LWC TypeScript Migration
View source1. Install with the skills CLI
npx skills add forcedotcom/sf-skills/experience-lwc-typescript-migrate --agent claude-code2. Or install it manually
Download the skill folder and drop it into ~/.claude/skills/ for all projects, or .claude/skills/ to scope it to one repo. Restart Claude Code so it picks up the new skill.
Anthropic's agentic coding CLI, and the reference implementation of Agent Skills. Drop a skill folder into ~/.claude/skills and Claude Code loads it automatically whenever a task matches the skill's description. Claude Code docs
Inside SKILL.md
Written by forcedotcomConverting 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
Frequently asked questions about LWC TypeScript Migration
Similar skills
React Composition Patterns
Streamline your React component architecture with proven patterns.
Pester Should Migration
Easily convert Pester v5 assertions to v6 syntax.
Radix to Base UI Migration
Seamlessly migrate React components from Radix UI to Base UI.
Migrate Next.js to Vinext
Seamlessly transition your Next.js projects to Vinext.
WinUI 3 Migration Guide
Streamline your UWP to WinUI 3 migration process.
Refactor
Enhance code maintainability without altering behavior.
