How it works
The explanation, with examples you can run.
What one looks like
// slugify.d.ts: types for a JavaScript file next to it, slugify.jsexport interface SlugOptions { separator?: string lower?: boolean} export declare function slugify(input: string, options?: SlugOptions): stringThere are no function bodies, only signatures. When your code imports ./slugify.js, the checker uses this file for the types and the runtime uses the JavaScript. If the JavaScript actually returns null for empty input, the declaration is wrong, and every caller trusts it anyway.
Where library types come from
- The package ships them. Its
package.jsonpoints at them ("types", or"types"conditions inside"exports"), usually generated bytsc --declarationfrom TypeScript source. This is the best case: they are released together with the code. - DefinitelyTyped. A large community repository of declarations for packages that don't ship their own, published to npm as
@types/<name>. Install it as a dev dependency. Its version is not tied to the library's, so check that the major versions line up. - You write them. For a package with no types anywhere, declare the parts you use:
// types/legacy-charts.d.tsdeclare module "legacy-charts" { export function draw(el: HTMLElement, data: number[]): void}The lib setting in tsconfig.json picks the built-in declarations (ES2023, DOM, and so on), and @types/node describes Node's APIs. A browser project that accidentally includes @types/node will happily type-check process.env in front-end code.
Globals and augmentation
Declarations can add to things that already exist:
// globals.d.tsexport {} declare global { interface Window { analytics?: { track(event: string): void } }}Interfaces with the same name merge, so this adds a property to the DOM's Window without replacing it. The same trick adds fields to a library's types, such as a user on a framework's request object.
Typing JavaScript you don't want to convert
TypeScript reads JSDoc comments in .js files:
// @ts-check /** * @param {number[]} prices * @returns {number} */export function total(prices) { return prices.reduce((sum, p) => sum + p, 0)}// @ts-check (or checkJs in the config) reports type errors in that file; allowJs lets TypeScript files import it; and declaration with emitDeclarationOnly generates .d.ts files from it. A library can stay in JavaScript and still give its users accurate types.
When library types are wrong
skipLibCheck: true is common because checking every .d.ts in node_modules is slow, but it also hides conflicts between them. When a declaration is wrong, fix it locally with module augmentation or a narrow cast at the call site, and send the fix upstream; casting the whole module to any throws away all its other types.
Check yourself
3 questions. Get them all right to finish this topic.
0 of 3 right so far. Get all three to finish this topic.