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:
/**
* 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 };
}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:
/**
* 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;
};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):
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)}`;
}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
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
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.jsonreports errors, but runtsc -p jsconfig.jsonin CI as well, since the two may include different files. - Point
typesin package.json at the output. The.d.tsfiles only reach users whenexports(ortypes) names them; see publishing a typed package. isolatedDeclarationsdoes not apply to JavaScript. It cannot be combined withallowJs(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
- Creating .d.ts Files from .js files · TypeScript handbook
- TSConfig Reference (declaration and emitDeclarationOnly) · TypeScript
Last reviewed 21 October 2026.