/commands/typescript/generate-declaration-files
tsc --declaration --emitDeclarationOnly: generate .d.ts files
- tool
- TypeScript
- versions
- TypeScript 2.8+ (emitDeclarationOnly), TypeScript 7.0
- os
- any
npx tsc --declaration --emitDeclarationOnly writes only the .d.ts type declaration files for your TypeScript sources, without any JavaScript. Use it when another tool (esbuild, swc, Babel, a bundler) produces the .js files for a library and you still need the types that consumers' editors and compilers read. Add --outDir to choose where they go.
Generate declarations
npm init -y >/dev/null
npm install --silent --no-audit --no-fund --save-dev typescript@7.0.2 >/dev/null 2>&1
mkdir src
cat > src/index.ts <<'EOF'
export interface Options { retries?: number }
/** Adds two numbers. */
export function add(a: number, b: number): number { return a + b; }
export const version = "1.0.0";
export async function fetchWithRetry(url: string, opts: Options = {}) {
return { url, retries: opts.retries ?? 3 };
}
EOF
npx tsc --declaration --emitDeclarationOnly --target es2022 --outDir types src/index.ts
find types src -type f | sort
cat types/index.d.tsOutput
src/index.ts
types/index.d.ts
export interface Options {
retries?: number;
}
/** Adds two numbers. */
export declare function add(a: number, b: number): number;
export declare const version = "1.0.0";
export declare function fetchWithRetry(url: string, opts?: Options): Promise<{
url: string;
retries: number;
}>;Only types/index.d.ts is written: the function bodies are gone, the inferred return type of fetchWithRetry is spelled out, and the JSDoc comment is kept for editor tooltips. The folder has no tsconfig.json, so naming the file on the command line is fine here; in a real project the options live in tsconfig.json (see below).
In tsconfig.json
{
"compilerOptions": {
"declaration": true,
"emitDeclarationOnly": true,
"declarationMap": true,
"outDir": "dist"
},
"include": ["src"]
}declarationMap adds .d.ts.map files, so "Go to definition" in an editor jumps to the .ts source instead of the declaration.
Related options
| Option | Effect |
|---|---|
| declaration | Emit a .d.ts file for every .ts or .js input (required for emitDeclarationOnly) |
| emitDeclarationOnly | Write the .d.ts files and no JavaScript |
| declarationDir | Put declarations in a different folder from outDir |
| declarationMap | Emit .d.ts.map files that map declarations back to sources |
| allowJs + declaration | Generate declarations for JavaScript files from JSDoc types |
| isolatedDeclarations | Require explicit types on exports so other tools can emit .d.ts files without type checking (TypeScript 5.5+) |
Pitfalls
tsc src/index.tswith a tsconfig.json present fails with error TS5112 on TypeScript 6.0+; runnpx tsc(or-p tsconfig.build.json) without file names, or pass--ignoreConfig.- noEmit wins. With
"noEmit": truein the config, nothing is written; use a separate build config without it. - Types of dependencies. If your exported types mention a package's types, consumers need that package (or its
@typespackage) too, or they get TS2307 Cannot find module. - package.json. Declarations are only found when
typesor thetypescondition in exports points at them.
Confirm it worked
List the output folder: there must be one .d.ts per source file and no .js files. Then import the package from a test project and hover a function in the editor to see the declared types.
FAQ
Can tsc generate .d.ts files for a JavaScript project?
Yes. Add allowJs and declaration (with emitDeclarationOnly) and tsc reads JSDoc comments to infer the types. The TypeScript handbook page on creating .d.ts files from .js files describes the setup.
How do consumers find the generated declarations?
Through the types field, or a types condition in exports, in package.json. Point it at the .d.ts file that matches the JavaScript entry point, for example dist/index.d.ts.
Why does tsc emit nothing with emitDeclarationOnly?
emitDeclarationOnly needs declaration (or composite) to be on; otherwise tsc reports an error and writes nothing. noEmit in tsconfig.json also wins over it, so remove noEmit.