Recipe · Frameworks and tests

How do I create .d.ts files from my JavaScript with tsc?

  • Library (ships .d.ts)
  • .d.ts
  • declaration
  • emitDeclarationOnly
  • Checked with TypeScript 5.9

The problem My library is plain JavaScript with JSDoc. People who install it get no types. How do I have tsc write .d.ts files from the JSDoc, without compiling or changing my .js files?

The short answer

Keep jsconfig.json for checking, and add a second config that extends it and turns on declaration output. Run it with tsc -p jsconfig.types.json:

src/price.js TypeScript 5.9.3 TypeScript 5.9.3, run 21 October 2026: No errors. Writes the .d.ts files shown. 6.0.3 : differs, see why TypeScript 6.0.3, run 21 October 2026: differs from 5.9.3. error TS5011: The common source directory of 'jsconfig.types.json' is './src'. The 'rootDir' setting must be explicitly set to this or another path to adjust your output's file layout. Visit https://aka.ms/ts6 for migration information. Writes different .d.ts files. 7.0.2 : differs, see why TypeScript 7.0.2, run 21 October 2026: differs from 5.9.3. jsconfig.types.json(7,5): error TS5011: The common source directory of 'jsconfig.types.json' is './src'. The 'rootDir' setting must be explicitly set to this or another path to adjust your output's file layout. Visit https://aka.ms/ts6 for migration information. Writes different .d.ts files. checked 21 October 2026
/**
 * A price in the smallest unit of its currency.
 * @typedef {object} Price
 * @property {number} cents
 * @property {string} currency The ISO code, such as "USD".
 */

/**
 * @param {Price} price
 * @param {string} [locale]
 * @returns {string}
 */
export function formatPrice(price, locale = "en-US") {
  return new Intl.NumberFormat(locale, { style: "currency", currency: price.currency }).format(price.cents / 100);
}

/**
 * @param {Price[]} prices
 * @param {string} currency
 * @returns {Price}
 */
export function sumPrices(prices, currency) {
  const cents = prices.filter((price) => price.currency === currency).reduce((total, price) => total + price.cents, 0);
  return { cents, currency };
}
The package source, with JSDoc types.
jsconfig.json for this example
{
  "compilerOptions": {
    "checkJs": true,
    "noEmit": true,
    "strict": true,
    "target": "es2022",
    "module": "nodenext",
    "types": []
  },
  "include": ["src/**/*.js"]
}

tsc writes one .d.ts per source file into types/. This is the file it writes for the source above, checked on every build of this site:

types/price.d.ts Written by TypeScript 5.9.3 TypeScript 5.9.3, run 21 October 2026: No errors. Writes the .d.ts files shown. 6.0.3 : differs, see why TypeScript 6.0.3, run 21 October 2026: differs from 5.9.3. error TS5011: The common source directory of 'jsconfig.types.json' is './src'. The 'rootDir' setting must be explicitly set to this or another path to adjust your output's file layout. Visit https://aka.ms/ts6 for migration information. Writes different .d.ts files. 7.0.2 : differs, see why TypeScript 7.0.2, run 21 October 2026: differs from 5.9.3. jsconfig.types.json(7,5): error TS5011: The common source directory of 'jsconfig.types.json' is './src'. The 'rootDir' setting must be explicitly set to this or another path to adjust your output's file layout. Visit https://aka.ms/ts6 for migration information. Writes different .d.ts files. checked 21 October 2026
/**
 * A price in the smallest unit of its currency.
 * @typedef {object} Price
 * @property {number} cents
 * @property {string} currency The ISO code, such as "USD".
 */
/**
 * @param {Price} price
 * @param {string} [locale]
 * @returns {string}
 */
export function formatPrice(price: Price, locale?: string): string;
/**
 * @param {Price[]} prices
 * @param {string} currency
 * @returns {Price}
 */
export function sumPrices(prices: Price[], currency: string): Price;
/**
 * A price in the smallest unit of its currency.
 */
export type Price = {
    cents: number;
    /**
     * The ISO code, such as "USD".
     */
    currency: string;
};
types/price.d.ts, as tsc writes it from the JavaScript.
jsconfig.types.json, the config that writes it
{
  "extends": "./jsconfig.json",
  "compilerOptions": {
    "noEmit": false,
    "declaration": true,
    "emitDeclarationOnly": true,
    "declarationDir": "types"
  }
}

Why it works

declaration: true asks tsc for .d.ts files, emitDeclarationOnly: true stops it from writing JavaScript (your .js files are already what runs), and declarationDir puts the output in its own folder. "noEmit": false matters: a config that extends a jsconfig.json inherits that file’s noEmit: true, and noEmit silently wins over emitDeclarationOnly. The @typedef becomes an exported type, and every JSDoc description is copied into the declaration file, so hovers in your users’ editors show it.

The same thing in TypeScript

The .d.ts file above is the TypeScript view: it is what a TypeScript author would write by hand for this module. A TypeScript library gets the same file from declaration: true in its tsconfig.json.

What goes wrong

Whatever has no JSDoc type is any in the declaration file, and your users get no help for it. With strict on, tsc reports those parameters before you publish (TS7006):

src/price.js Error confirmed 5.9.3 TypeScript 5.9.3, run 21 October 2026: src/price.js(2,29): error TS7006: Parameter 'cents' implicitly has an 'any' type. src/price.js(2,36): error TS7006: Parameter 'currency' implicitly has an 'any' type. 6.0.3 TypeScript 6.0.3, run 21 October 2026: the same as 5.9.3. src/price.js(2,29): error TS7006: Parameter 'cents' implicitly has an 'any' type. src/price.js(2,36): error TS7006: Parameter 'currency' implicitly has an 'any' type. 7.0.2 TypeScript 7.0.2, run 21 October 2026: the same as 5.9.3. src/price.js(2,29): error TS7006: Parameter 'cents' implicitly has an 'any' type. src/price.js(2,36): error TS7006: Parameter 'currency' implicitly has an 'any' type. checked 21 October 2026
export function formatPrice(cents, currency) {error TS7006: Parameter 'cents' implicitly has an 'any' type.error TS7006: Parameter 'currency' implicitly has an 'any' type.
  return `${currency} ${(cents / 100).toFixed(2)}`;
}
An exported function without JSDoc, under strict.
jsconfig.json for this example
{
  "compilerOptions": {
    "checkJs": true,
    "noEmit": true,
    "strict": true,
    "target": "es2022",
    "module": "nodenext",
    "types": []
  },
  "include": ["src/**/*.js"]
}

Under TypeScript 6.0 and 7.0

Every example on this page also runs under TypeScript 6.0.3 and 7.0.2 when this site is built. One of them reports something different from 5.9.3, the version the page describes; the ✗ on its badge links to its entry below.

rootDir defaults to the config's folder

TypeScript 6.0 and 7.0 · defaults that changed in 6.0

The pattern: A package that writes .d.ts files from src/ with declarationDir and no rootDir.

Why: Only the 6.0 default for rootDir causes this. 5.9 took the folder that holds every source file (src/), so src/price.js became types/price.d.ts. From 6.0 the default is the folder of the config, so the same file becomes types/src/price.d.ts, and tsc reports TS5011 asking for an explicit rootDir. The .d.ts that 6.0.3 writes is otherwise the same file. Source: TypeScript 6.0 release notes: rootDir now defaults to ..

The package source, with JSDoc types src/price.js

TypeScript 5.9.3

No errors.

Writes the .d.ts files the example shows.

TypeScript 6.0.3differs from 5.9.3
error TS5011: The common source directory of 'jsconfig.types.json' is './src'. The 'rootDir' setting must be explicitly set to this or another path to adjust your output's file layout.
  Visit https://aka.ms/ts6 for migration information.
The file it writes: types/src/price.d.ts
/**
 * A price in the smallest unit of its currency.
 * @typedef {object} Price
 * @property {number} cents
 * @property {string} currency The ISO code, such as "USD".
 */
/**
 * @param {Price} price
 * @param {string} [locale]
 * @returns {string}
 */
export function formatPrice(price: Price, locale?: string): string;
/**
 * @param {Price[]} prices
 * @param {string} currency
 * @returns {Price}
 */
export function sumPrices(prices: Price[], currency: string): Price;
/**
 * A price in the smallest unit of its currency.
 */
export type Price = {
    cents: number;
    /**
     * The ISO code, such as "USD".
     */
    currency: string;
};
TypeScript 7.0.2differs from 5.9.3
jsconfig.types.json(7,5): error TS5011: The common source directory of 'jsconfig.types.json' is './src'. The 'rootDir' setting must be explicitly set to this or another path to adjust your output's file layout.
  Visit https://aka.ms/ts6 for migration information.
The file it writes: types/src/price.d.ts
/**
 * A price in the smallest unit of its currency.
 * @typedef {object} Price
 * @property {number} cents
 * @property {string} currency The ISO code, such as "USD".
 */
export type Price = {
    cents: number;
    /**
     * The ISO code, such as "USD".
     */
    currency: string;
};
/**
 * @param {Price} price
 * @param {string} [locale]
 * @returns {string}
 */
export declare function formatPrice(price: Price, locale?: string): string;
/**
 * @param {Price[]} prices
 * @param {string} currency
 * @returns {Price}
 */
export declare function sumPrices(prices: Price[], currency: string): Price;

What works in every version: Set "rootDir": "src" in jsconfig.types.json, which gives the 5.9 layout in every version.

7.0 writes different .d.ts files from JavaScript

TypeScript 7.0 · the .d.ts files written from JavaScript

The pattern: A package that writes its .d.ts files from JSDoc with declaration and emitDeclarationOnly.

Why: The port rewrote declaration output for JavaScript, and its list of changes says matching the old output exactly is not a goal. With rootDir set to src, TypeScript 7.0.2 still writes different files from 5.9's: a @typedef becomes an export type where its comment is (5.9 puts it last), functions and classes are export declare, an exported object is a const rather than a namespace, and optional properties lose their | undefined. Source: CHANGES.md: Declaration Emit.

The package source, with JSDoc types src/price.js

TypeScript 5.9.3

No errors.

Writes the .d.ts files the example shows.

TypeScript 6.0.3differs from 5.9.3
error TS5011: The common source directory of 'jsconfig.types.json' is './src'. The 'rootDir' setting must be explicitly set to this or another path to adjust your output's file layout.
  Visit https://aka.ms/ts6 for migration information.
The file it writes: types/src/price.d.ts
/**
 * A price in the smallest unit of its currency.
 * @typedef {object} Price
 * @property {number} cents
 * @property {string} currency The ISO code, such as "USD".
 */
/**
 * @param {Price} price
 * @param {string} [locale]
 * @returns {string}
 */
export function formatPrice(price: Price, locale?: string): string;
/**
 * @param {Price[]} prices
 * @param {string} currency
 * @returns {Price}
 */
export function sumPrices(prices: Price[], currency: string): Price;
/**
 * A price in the smallest unit of its currency.
 */
export type Price = {
    cents: number;
    /**
     * The ISO code, such as "USD".
     */
    currency: string;
};
TypeScript 7.0.2differs from 5.9.3
jsconfig.types.json(7,5): error TS5011: The common source directory of 'jsconfig.types.json' is './src'. The 'rootDir' setting must be explicitly set to this or another path to adjust your output's file layout.
  Visit https://aka.ms/ts6 for migration information.
The file it writes: types/src/price.d.ts
/**
 * A price in the smallest unit of its currency.
 * @typedef {object} Price
 * @property {number} cents
 * @property {string} currency The ISO code, such as "USD".
 */
export type Price = {
    cents: number;
    /**
     * The ISO code, such as "USD".
     */
    currency: string;
};
/**
 * @param {Price} price
 * @param {string} [locale]
 * @returns {string}
 */
export declare function formatPrice(price: Price, locale?: string): string;
/**
 * @param {Price[]} prices
 * @param {string} currency
 * @returns {Price}
 */
export declare function sumPrices(prices: Price[], currency: string): Price;

What works in every version: Compare the .d.ts files before releasing with 7.0, and commit the new ones once they read right.

Gotchas

  • Run the check config too. tsc -p jsconfig.types.json reports errors, but run tsc -p jsconfig.json in CI as well, since the two may include different files.
  • Point types in package.json at the output. The .d.ts files only reach users when exports (or types) names them; see publishing a typed package.
  • isolatedDeclarations does not apply to JavaScript. It cannot be combined with allowJs (TS5053), so JavaScript libraries keep using tsc’s full declaration output.

Questions

Why a second config file instead of setting declaration in jsconfig.json?

A file named jsconfig.json defaults to noEmit: true, which editors and tsc -p jsconfig.json rely on for checking. A separate jsconfig.types.json that extends it turns emit on for one job, writing types, and nothing else.

Should the .d.ts files be committed?

Usually not: write them in a prepack or build script so the published package always has fresh ones, and list the folder in .gitignore. This site's example commits them only so the build can compare them with what tsc writes.

Sources

Last reviewed 21 October 2026.