Skip to main content
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

This will modify your source code. Commit any unsaved changes before running.
NODE_OPTIONS="--experimental-import-meta-resolve" \
  npx codemod @nodejs/correct-ts-specifiers
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.

Examples

Basic Import Corrections

Before
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');
After
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:
Enable rewriteRelativeImportExtensions in your tsconfig:
tsconfig.json
{
  "compilerOptions": {
    "rewriteRelativeImportExtensions": true
  }
}

Monorepo Usage

For monorepos, run the codemod within each workspace for best results:
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

The codemod doesn’t blindly replace extensions - it confirms each replacement actually exists on the filesystem.
  • 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
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.

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

Build docs developers (and LLMs) love