> ## Documentation Index
> Fetch the complete documentation index at: https://mintlify.com/nodejs/userland-migrations/llms.txt
> Use this file to discover all available pages before exploring further.

# Correct TypeScript Specifiers

> Transform TypeScript import specifiers from .js extensions to correct .ts/.cts/.mts extensions for Node.js ESM compatibility

This recipe corrects TypeScript import specifiers from the old `tsc` requirement of using `.js` extensions in source code to proper TypeScript extensions, making your code runnable by Node.js and other standards-compliant runtime environments.

## What It Does

Transforms import specifiers to use correct file extensions:

* No extension → `.ts`, `.cts`, `.mts`, `.d.ts`, `.d.cts`, `.d.mts`
* `.js` → `.ts`
* `.cjs` → `.cts`
* `.mjs` → `.mts`
* `.js` → `.d.ts`, `.d.cts`, `.d.mts` (for type-only imports)
* Directory imports → `./directory/index.ts`

## Usage

<Warning>
  This will modify your source code. Commit any unsaved changes before running.
</Warning>

```bash theme={null}
NODE_OPTIONS="--experimental-import-meta-resolve" \
  npx codemod @nodejs/correct-ts-specifiers
```

<Tip>
  The `--experimental-import-meta-resolve` flag is required for the codemod to resolve module paths correctly. Despite the name, this feature is stable in modern Node.js versions.
</Tip>

## Examples

### Basic Import Corrections

```typescript Before theme={null}
import { URL } from 'node:url';

import { bar } from '@dep/bar';
import { foo } from 'foo';

import { Bird } from './Bird';          // a directory
import { Cat } from './Cat.ts';
import { Dog } from '…/Dog/index.mjs';  // tsconfig paths
import { baseUrl } from '#config.js';   // package.json imports

export { Zed } from './zed';

export const makeLink = (path: URL) => (new URL(path, baseUrl)).href;

const nil = await import('./nil.js');

const bird = new Bird('Tweety');
const cat = new Cat('Milo');
const dog = new Dog('Otis');
```

```typescript After theme={null}
import { URL } from 'node:url';

import { bar } from '@dep/bar';
import { foo } from 'foo';

import { Bird } from './Bird/index.ts';
import { Cat } from './Cat.ts';
import { Dog } from '…/Dog/index.mts';  // tsconfig paths
import { baseUrl } from '#config.js';   // package.json imports

export type { Zed } from './zed.d.ts';

export const makeLink = (path: URL) => (new URL(path, baseUrl)).href;

const nil = await import('./nil.ts');

const bird = new Bird('Tweety');
const cat = new Cat('Milo');
const dog = new Dog('Otis');
```

## Supported Cases

* **Missing extensions** - Adds correct `.ts`, `.cts`, `.mts`, or `.d.ts` extensions
* **Incorrect extensions** - Changes `.js` to `.ts`, `.cjs` to `.cts`, `.mjs` to `.mts`
* **Package.json subpath imports** - Respects `#` imports defined in package.json
* **tsconfig paths** - Resolves path aliases via `@nodejs-loaders/alias`
* **Directory specifiers** - Converts `./dir` to `./dir/index.ts`
* **Type-only exports** - Automatically adds `type` keyword where appropriate

## TypeScript Configuration

After running this codemod, update your `tsconfig.json`:

<Tabs>
  <Tab title="Using tsc for Compilation">
    Enable `rewriteRelativeImportExtensions` in your tsconfig:

    ```json tsconfig.json theme={null}
    {
      "compilerOptions": {
        "rewriteRelativeImportExtensions": true
      }
    }
    ```
  </Tab>

  <Tab title="Using tsc for Type-Checking Only">
    Enable `allowImportingTsExtensions` and `noEmit`:

    ```json tsconfig.json theme={null}
    {
      "compilerOptions": {
        "allowImportingTsExtensions": true,
        "noEmit": true
      }
    }
    ```
  </Tab>
</Tabs>

## Monorepo Usage

For monorepos, run the codemod **within each workspace** for best results:

```text theme={null}
project-root/
  ├ workspaces/
    ├ foo/ ← RUN HERE
      ├ …
      ├ package.json
      └ tsconfig.json
    └ bar/ ← RUN HERE
      ├ …
      ├ package.json
      └ tsconfig.json
  └ utils/ ← RUN HERE
    ├ qux.js
    └ zed.js
```

## Safety Features

<Tip>
  The codemod doesn't blindly replace extensions - it confirms each replacement actually exists on the filesystem.
</Tip>

* **File existence verification** - Only applies changes when target files exist
* **Ambiguity detection** - Skips cases where multiple valid targets exist (e.g., both `foo.ts` and `foo.js`)
* **Error logging** - Reports ambiguous cases for manual review
* **Continues on errors** - Skips problematic imports and continues processing

<Warning>
  The codemod does not verify that imported modules contain the expected exports. Run your code after migration to catch any issues - Node.js will error if imports are incorrect.
</Warning>

## Type Import Keywords

Node.js requires the `type` keyword on type-only imports. While this codemod handles most cases, some scenarios may require additional tooling:

* [`use-import-type` via Biome](https://biomejs.dev/linter/rules/use-import-type/)
* [`typescript/no-import-type-side-effects` via oxlint](https://oxc.rs/docs/guide/usage/linter/rules/typescript/no-import-type-side-effects)
* [`consistent-type-imports` via typescript-eslint](https://typescript-eslint.io/rules/consistent-type-imports)

Run this codemod first, then apply one of these fixers if needed.

## Why This Matters

TypeScript historically required `.js` extensions in import statements even when importing `.ts` files, creating a mismatch between source code and actual files. With Node.js native ESM support:

* Import specifiers must match actual file names
* Runtime environments need correct extensions to resolve modules
* This enables running TypeScript directly with loaders or transpilers
* Aligns with ECMAScript module standards
