---
name:        js-to-typescript
description: "Converts one JavaScript source file to strict TypeScript 5.x, resolves all tsc errors without @ts-ignore, and confirms the Jest suite is unchanged."
metadata:
  phase:           4
  source_stack:    "JavaScript ES2020+, CommonJS or ESM, no JSDoc, no strict mode"
  target_stack:    "TypeScript 5.x, strict: true, ESM"
  effort_estimate: M
  last_updated:    2026-04-04
---

# 1. Purpose

This skill converts a single JavaScript source file (ES2020+) to strict-mode TypeScript 5.x
as one atomic step in an incremental file-by-file migration. It is run by a frontend or
full-stack engineer once per file, in isolation from all other files — never applied to the
whole repo in a single pass. The coexistence strategy relies on `allowJs: true` in tsconfig,
which lets unconverted `.js` files continue compiling alongside newly converted `.ts` files
throughout the transition. The primary risks are implicit `any` widening — where removing
a JSDoc annotation or leaving an untyped parameter silently degrades type safety across
callers — and engineers suppressing tsc errors with `@ts-ignore` to meet a deadline, which
converts a type error into permanent type debt. Both are treated as hard failures by this skill.

---

# 2. Trigger Conditions

**Use when:**
- `target_file` is a `.js` source file under `src/` (not a test file, not a config file).
- `tsconfig.json` in `repo_root` already has `"allowJs": true` and `"strict": true` — Phase 3 preparation is complete.
- `ts-jest` (or equivalent) is configured so Jest runs `.ts` and `.js` files without a config change between them.
- `npx jest` exits 0 on the current commit — the suite is green before you start.
- The file appears in the migration manifest produced by `/revamp-spec`.
- All TypeScript files already in `src/` compile cleanly (`npx tsc --noEmit` exits 0 before this run).

**Do NOT use when:**
- `target_file` is a test file (`*.test.js`, `*.spec.js`, `__tests__/*.js`) — test files must be converted only after every source file they import has been converted.
- `target_file` has more than 300 lines and zero test coverage — flag for manual review before converting; the equivalence tests cannot catch regressions in untested code.
- `npx tsc --noEmit` currently reports errors on any `.ts` file in the repo — do not add new `.ts` files on top of existing unresolved errors.
- Phase 3 preparation (`tsconfig` setup, `ts-jest` config, `allowJs: true`) is not yet complete — run `skills/03-prepare/` first.
- You are considering converting more than one file in this skill invocation — run the skill once per file; parallelism here creates merge conflicts and tangled error attribution.

---

# 3. Inputs

**Required:**

| Input | Type | Description |
|-------|------|-------------|
| `target_file` | file-path | Repo-root-relative path to the `.js` file to convert. Example: `src/utils/parseDate.js`. Must exist and be a source file, not a test. |
| `repo_root` | file-path | Absolute path to the repository root. Used as the working directory for all `tsc` and `jest` invocations. |

**Optional:**

| Input | Type | Default | Description |
|-------|------|---------|-------------|
| `max_tsc_errors` | integer | `20` | If `tsc --noEmit` reports more errors than this threshold after renaming the file, revert and flag for manual review. Raise for large, complex files; lower for small utility files. |
| `jest_pattern` | string | Basename of `target_file` without extension | Passed to `jest --testPathPattern`. Override when the test file's name doesn't match the source file's basename. Example: `parseDate` resolves to tests named `parseDate.test.js`. |
| `dry_run` | boolean | `false` | When `true`: read and analyze the file, write the any-map and symbol inventory, but do not rename or modify any source file. Useful for previewing the annotation burden before committing to conversion. |

<!--
  STOP CONDITION: If `target_file` does not exist, halt immediately:
  "I need a valid target_file path to proceed. Check that the file exists at <repo_root>/<target_file>."
  If `repo_root` is not provided, halt:
  "I need repo_root (the absolute path to the repository root) to invoke tsc and jest correctly."
-->

---

# 4. Steps

1. Read `<repo_root>/tsconfig.json`. Confirm both `"allowJs": true` and `"strict": true` are present in the compiler options.
   - If either is missing: STOP. Do not proceed. Ask: "tsconfig.json is missing `<field>`. This skill requires both `allowJs: true` and `strict: true` to be set before conversion. Should I add them, or should we run `skills/03-prepare/` first?"

2. Run `npx jest --passWithNoTests --testPathPattern=<jest_pattern> --no-coverage` from `<repo_root>`. Record the full output and exit code as **`pre_jest_result`**.
   - If exit code is non-zero: STOP. The test suite is already failing before conversion. Report the failing tests and do not touch `target_file`. The engineer must fix the suite first.

3. Run `npx tsc --noEmit` from `<repo_root>`. Record the full output and error count as **`pre_tsc_result`**.
   - If any error in `pre_tsc_result` references `target_file`: STOP. Ask: "tsc already reports errors in `<target_file>` before conversion. Address these first, or exclude this file from the current run."
   - Record the total pre-conversion error count for comparison in Step 8.

4. Read `target_file`. Scan every function parameter, variable declaration, and function return position that lacks an explicit type annotation and would become `implicit any` under `strict: true`. Write each occurrence to `output/js-to-typescript-any-map-<timestamp>.json` with this shape per entry: `{ "line": <N>, "identifier": "<name>", "kind": "param|var|return", "context": "<surrounding 1–2 lines>" }`.

5. → Hand off to `code-archaeologist` (see Section 5. Agent Handoffs). Wait for `output/js-to-typescript-inventory-<timestamp>.md` to be written before continuing.

6. Read `output/js-to-typescript-inventory-<timestamp>.md`. For each exported symbol listed:
   a. Derive the narrowest accurate TypeScript type based on call-site evidence in the inventory. Acceptable types: specific primitives, interfaces, union types, generics. Not acceptable: `any`, `object`, `Function`, `{}`.
   b. If a type cannot be determined from usage evidence alone, use `unknown` and insert a line comment `// TODO(ts-migration): narrow — <reason why type is uncertain>`. This is the only acceptable placeholder; never use `@ts-ignore`.
   c. Record the chosen type for each exported symbol in the migration log.

7. If `dry_run` is `true`: write the migration log with the proposed annotations and STOP. Do not rename or modify the source file.

8. Rename `<target_file>` from `.js` to `.ts` (e.g., `src/utils/parseDate.js` → `src/utils/parseDate.ts`). Apply all type annotations derived in Step 6. Write the annotated file.
   - If the rename or write fails: restore from git (`git checkout -- <target_file>`), write the failure reason to the migration log, STOP.

9. Run `npx tsc --noEmit` from `<repo_root>`. Count errors. Record full output in the migration log.
   - If error count > `max_tsc_errors`: revert the rename (`git mv src/.../parseDate.ts src/.../parseDate.js && git checkout -- src/.../parseDate.js`), write a revert entry to the migration log, and STOP with: "Reverted `<target_file>` — <N> tsc errors exceeded threshold of <max_tsc_errors>. File flagged for manual review in migration log."
   - If error count is 0–`max_tsc_errors`: categorize each error as one of: `implicit-any` | `missing-property` | `incompatible-return` | `type-assertion-needed` | `other`. Log the category and line for each.

10. Address each tsc error from Step 9, one at a time, in order of category: `implicit-any` first, then `missing-property`, then `incompatible-return`, then the rest. For each error:
    a. Read the error message and the surrounding code context (±5 lines).
    b. Apply the narrowest correct type fix. Do not use `@ts-ignore`. Do not use `as any`.
    c. After fixing, re-run `npx tsc --noEmit --noEmit` and confirm the specific error is gone before moving to the next.
    - If an individual error persists after three distinct fix attempts: leave a `// TODO(ts-migration): unresolved — <tsc error text>` comment at the offending line, log the error as `needs-manual-review` in the migration log, and continue to the next error. Do not block on it.

11. Run `npx tsc --noEmit` from `<repo_root>` one final time. Confirm exit code is 0 and error count referencing the converted file is 0. Log result.
    - If errors remain: for any error without a `// TODO(ts-migration):` comment, return to Step 10. If all remaining errors have TODO comments, proceed — the manual-review flags are intentional and logged.

12. Grep for `@ts-ignore` in the converted `.ts` file: `grep -n '@ts-ignore' <repo_root>/<stem>.ts`.
    - If any match is found: this is a hard failure. Remove each `@ts-ignore`, resolve the underlying error properly (return to Step 10 logic), or escalate to manual review with a TODO comment. Do not proceed to Step 13 until the file is `@ts-ignore`-free.

13. Run `npx jest --passWithNoTests --testPathPattern=<jest_pattern> --no-coverage` from `<repo_root>`. Record full output and exit code as **`post_jest_result`**.
    - If exit code is non-zero: revert the `.ts` file (`git checkout -- <repo_root>/<stem>.ts`; then `git mv` back to `.js`), write failure details to the migration log, STOP with: "Jest suite failed after conversion — reverted. See migration log for failing test names."

14. Write all outputs declared in Section 7. Run every Equivalence Test in Section 6 and record each result (pass/fail + evidence) in `output/js-to-typescript-equiv-<timestamp>.md`. Evaluate every item in Section 9 Done Criteria; report pass/fail inline, then print the final verdict.

---

# 5. Agent Handoffs

## code-archaeologist

- **File:** `agents/code-archaeologist.md`
- **Triggered by:** Step 5
- **Prompt template:**
  ```
  TASK: Inventory all exported symbols in <target_file> and trace their import sites
        across the repository. For each exported symbol, report:
          - name and kind (function | class | const | type alias)
          - parameter names and any type information inferable from JSDoc, usage patterns,
            or argument shapes at call sites
          - return type if inferable from return statements or call-site assignments
          - file path and line number of every import site in SCOPE
        Flag any symbol imported but never called (dead export).
        Flag any symbol where callers pass heterogeneous argument types (type is ambiguous).
  REPO_ROOT:   <repo_root>
  SCOPE:       <repo_root>/src
  OUTPUT_FILE: output/js-to-typescript-inventory-<timestamp>.md
  FORMAT:      markdown
  ```

---

# 6. Equivalence Tests

<!--
  All six tests must be run in Step 14. Results are written to output/js-to-typescript-equiv-<timestamp>.md.
  "Pre-conversion baseline" refers to pre_jest_result captured in Step 2.
-->

| Test Name | Input | Expected Output | Tool |
|-----------|-------|-----------------|------|
| `jest-pre` | `npx jest --passWithNoTests --testPathPattern=<jest_pattern> --no-coverage` run against the original `.js` file (Step 2 result) | Exit code 0. Record test count (passed/failed/skipped) as the baseline for `jest-post`. | Bash — result already captured in Step 2 as `pre_jest_result`. |
| `jest-post` | `npx jest --passWithNoTests --testPathPattern=<jest_pattern> --no-coverage` run against the converted `.ts` file (Step 13 result) | Exit code 0. Passed/failed/skipped counts are identical to `jest-pre` baseline. Any count divergence is a fail, even if exit code is 0. | Bash — result already captured in Step 13 as `post_jest_result`. |
| `tsc-clean` | `npx tsc --noEmit` from `<repo_root>` after all Step 10–11 fixes | Exit code 0; 0 errors in output. A non-zero exit or any error line referencing `<stem>.ts` is a fail. | Bash — result already captured in Step 11. |
| `no-ts-ignore` | `grep -n '@ts-ignore' <repo_root>/<stem>.ts` | No output — grep exits 1 (no matches). Any match is a hard fail. | Bash |
| `no-implicit-any` | `npx tsc --noEmit --noImplicitAny` from `<repo_root>`, output filtered to lines referencing `<stem>.ts` | 0 error lines referencing `<stem>.ts`. | Bash |
| `exports-unchanged` | Read the symbol inventory from `output/js-to-typescript-inventory-<timestamp>.md`; grep `<repo_root>/<stem>.ts` for each exported name | Every name exported from the original `.js` is exported from the `.ts` file. No export is removed or renamed. Narrowed types are acceptable; widened types (e.g., a previously-typed return becoming `any` or `unknown`) are a fail. | Bash + Read |

---

# 7. Outputs

| Artifact | Path Pattern | Format | Description |
|----------|-------------|--------|-------------|
| Implicit-any map | `output/js-to-typescript-any-map-<timestamp>.json` | json | Pre-conversion list of every position that would become implicit `any` under strict mode, with line number, identifier, and kind. Consumed in Step 6 to guide annotation; retained as evidence that all implicit-any positions were addressed. |
| Symbol inventory | `output/js-to-typescript-inventory-<timestamp>.md` | markdown | Produced by `code-archaeologist` in Step 5. Lists every exported symbol, its call sites, and inferred types. Consumed in Step 6 and by the PR reviewer to verify no exports were silently altered. |
| Migration log | `output/js-to-typescript-log-<timestamp>.md` | markdown | Chronological record of every tsc error encountered (category, line, resolution), any manual-review flags, the final tsc error count before and after, and the assumptions list and confidence level. Consumed by `/validate` and by the engineer reviewing the PR. |
| Equivalence test results | `output/js-to-typescript-equiv-<timestamp>.md` | markdown | Pass/fail verdict for every row in Section 6, with the raw command output or evidence attached. Required by Section 9 Done Criteria. |

---

# 8. References

- `references/migration-anti-patterns.md` — "Confidence Without Evidence" (§7) applies to every type annotation in Step 6; "Skipping Equivalence Validation" (§3) is why `jest-post` is a hard gate.
- `references/stack-compatibility-matrix.md` — "Runtime / Language Compatibility: Node.js 16→20" row; ESM/CJS interop notes apply if the repo mixes module systems.
- `skills/03-prepare/` — tsconfig scaffolding, ts-jest configuration, and `allowJs: true` setup must be complete before this skill runs.
- `skills/05-validate/` — run `/validate` after all `.js` files in a module or feature boundary are converted to confirm end-to-end behavioral equivalence at the module level.
- `agents/code-archaeologist.md` — the agent invoked in Step 5; review its output format before debugging unexpected inventory results.
- `https://www.typescriptlang.org/docs/handbook/migrating-from-javascript.html` — official incremental migration guide; the `allowJs` + `strict` coexistence approach this skill uses is documented there with additional rationale.

---

# 9. Done Criteria

<!--
  Claude evaluates each item and reports pass/fail before declaring this skill complete.
  Any unchecked item means the skill is NOT complete — do not mark it done.
  Skill-specific gates (1–10) are above the mandatory universal gates (11–15).
-->

- [ ] `target_file` no longer exists as a `.js` file — `ls <repo_root>/<stem>.js` exits with "No such file or directory" (or equivalent). If `dry_run` was `true`, this gate does not apply; skip and note.
- [ ] The converted `.ts` file exists at `<repo_root>/<stem>.ts` — file read succeeds and returns the annotated content from Step 8.
- [ ] `npx tsc --noEmit` exits 0 with 0 errors referencing `<stem>.ts` — confirmed by Step 11 output in the migration log.
- [ ] No `@ts-ignore` in `<stem>.ts` — `grep -n '@ts-ignore' <repo_root>/<stem>.ts` returns no output (`no-ts-ignore` equivalence test is pass).
- [ ] No `as any` cast introduced in `<stem>.ts` that was not present in the original `.js` — `grep -n ' as any' <repo_root>/<stem>.ts` output compared against pre-conversion any-map; any new occurrence is a fail.
- [ ] `jest-post` equivalence test recorded as **pass** in `output/js-to-typescript-equiv-<timestamp>.md` — test counts match `jest-pre` baseline exactly.
- [ ] `tsc-clean` equivalence test recorded as **pass** — exit code 0, 0 error lines.
- [ ] `no-ts-ignore` equivalence test recorded as **pass** — grep returned no matches.
- [ ] `no-implicit-any` equivalence test recorded as **pass** — 0 errors referencing `<stem>.ts`.
- [ ] `exports-unchanged` equivalence test recorded as **pass** — every exported name from the `.js` inventory is present and exported in the `.ts` file.
- [ ] All output files listed in Section 7 exist at their declared paths — verify each with a file read.
- [ ] Every equivalence test in Section 6 has a recorded result in `output/js-to-typescript-equiv-<timestamp>.md` — no test name is missing from the results file.
- [ ] No equivalence test in Section 6 is recorded as **fail** — grep the results file for `fail`; zero matches required.
- [ ] The migration log includes a confidence level (High / Medium / Low) — grep `output/js-to-typescript-log-<timestamp>.md` for `Confidence:`.
- [ ] The migration log includes a numbered assumptions list — grep `output/js-to-typescript-log-<timestamp>.md` for `Assumptions:`.
