Skip to main content
skill CX Codex

experience-lwc-typescript-migrate — Claude Code skill: LWC

Converts an existing JavaScript Lightning Web Component to TypeScript: a typed .ts plus a .d.ts that exposes only the @api surface.

Install into Claude Code · Skills CLI (Vercel) All methods and platforms ↓
npx skills add forcedotcom/sf-skills --skill experience-lwc-typescript-migrate -a claude-code
Open on GitHub

What experience-lwc-typescript-migrate does

The skill takes a working LWC bundle (componentName.js, .html, .css) and turns it into TypeScript. You get two files back: a fully typed .ts and a .d.ts that declares the c/componentName module and keeps only the public @api members. Private state and internal methods stay out of the declaration file.

The workflow runs in order. First it reads every file in the bundle and lists the @api fields and methods. Then it renames the file with git mv — a plain mv breaks the history link. Types come next: complex shapes become interface or type aliases, click handlers are typed as MouseEvent, async methods return Promise<T>, and any is replaced by unknown narrowed with a type guard. JSDoc hints get checked against the actual code before being trusted, since they often lie.

After that comes tsc --noEmit with no @ts-ignore patches, then the component's existing Jest tests. A bundled scripts/find-consumers.sh runs unconditionally to find other components importing this one — empty output is a valid result, not a reason to skip. You need git, jq and tsc 4.0.0 or newer, and the component has to build and pass tests in JavaScript first. Authoring a new LWC or writing its Jest tests is a different job, handled by experience-lwc-generate.

What it helps with

  • move an LWC bundle from .js to .ts without losing git history
  • produce a .d.ts with nothing but the @api surface
  • replace JSDoc hints with real TypeScript types
  • finish typing an already-renamed .ts that still has implicit any
  • check that consumers of the component still match the new public contract

Example prompts for the agent

  • convert this LWC to TypeScript and generate a .d.ts with only @api members
  • rename .js to .ts for this component and type-annotate the @api properties
  • produce a declare module 'c/accountTile' definition for this component

How to install experience-lwc-typescript-migrate

1Skills CLI (Vercel)

npx skills add forcedotcom/sf-skills --skill experience-lwc-typescript-migrate -a claude-code

2Manually: put the files in place

~/.claude/skills/experience-lwc-typescript-migrate/SKILL.md

Copy the whole skill folder; for one project use .claude/skills/experience-lwc-typescript-migrate/

Step by step, with commands for each platform: How to install skills in Claude Code: five ways

Questions about experience-lwc-typescript-migrate

How do I install experience-lwc-typescript-migrate in Claude?

Run npx skills add forcedotcom/sf-skills --skill experience-lwc-typescript-migrate -a claude-code. The skill file then lands at ~/.claude/skills/experience-lwc-typescript-migrate/SKILL.md.

What does experience-lwc-typescript-migrate do?

It converts an existing Lightning Web Component from JavaScript to TypeScript, typing the .ts and creating a matching .d.ts that declares the c/componentName module with only @api members.

How is it different from experience-lwc-generate?

This skill only handles a component that already exists in JavaScript. Authoring a brand-new LWC or generating Jest tests for one is experience-lwc-generate; migrating an Aura component to LWC is out of scope too.

Source text

SKILL.md · Apache-2.0
---
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
  `.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.

## 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).

---

## Workflow

### Step 1 — Read the component

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?

### Step 2 — Rename `.js` → `.ts` using `git mv`

```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.

### 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:

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.

#### 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 `@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.

### Step 5 — Compile and test

- 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.

### Step 6 — Expected final bundle shape

```text
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 `@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.

---

## 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.

## Support Resources

- [LWC TypeScript docs](https://developer.salesforce.com/docs/platform/lwc/guide/ts.html)
- [TypeScript Handbook](https://www.typescriptlang.org/docs/)
- [LWC Developer Guide](https://developer.salesforce.com/docs/component-library/documentation/en/lwc)

© forcedotcom · Apache-2.0 · forcedotcom/sf-skills

Similar skills

Everything for Frontend and UI →

From the same set185

service-helpagent-coordinate Walks you through standing up a Salesforce Help Agent: identity, Knowledge grounding, channel, go-live. Useful when the support agent has to live in Service Cloud. CX Codex +1 2,400installs service-digital-engagement-deployment-configure Configures Salesforce Embedded Messaging for In-App and Web (MIAW) deployments via Connect API and Metadata API. Useful when setting up chat widgets or updating metadata. CX Codex +1 2,400installs service-digital-engagement-channel-configure Configures and deploys Salesforce MIAW messaging channels. Use it when chat routing, user verification, or pre-chat settings need to be defined as metadata. CX Codex +1 2,400installs dx-devops-promote Runs DevOps Center promotion workflows for work items, from validation and preparation through promotion to a target pipeline stage. CX Codex +1 2,300installs experience-lwc-rtl-validate Reviews Lightning Web Components for right-to-left layout compliance. Scans CSS, markup, and scripts to generate code fixes for Arabic and Hebrew locales. CX Codex +1 2,300installs automation-sandbox-post-copy-config-generate Turns a Salesforce sandbox-refresh SOP into the JSON array the post-copy automation tool reads. Useful when the runbook lives in a PDF, spreadsheet or screenshot. CX Codex +1 2,300installs platform-mcp-tool-widget-coordinate Coordinates Lightning Type and HXL widget generation so a custom MCP server tool backed by an Apex Invocable Action renders as rich UI. CX Codex +1 2,300installs experience-ui-bundle-localize Extracts hardcoded UI strings from React or Angular UI Bundles into Salesforce Custom Labels and wires runtime i18n through the GraphQL UI API. CX Codex +1 2,200installs

Project sponsors

Companies that keep this analytics open
Gold sponsor

Impressions, clicks and code copies — open statistics. We keep the numbers public for the same reason NeBlask itself is public. Become a sponsor