Declaration Files and DefinitelyTyped
How .d.ts files describe JS to TypeScript, when to use @types packages, module augmentation, and writing minimal ambient types for untyped libs.
- typescript
- declaration-files
TypeScript type-checks code it can see types for. Pure JavaScript libraries ship either built-in types ("types" in package.json) or community types from DefinitelyTyped (@types/...). Declaration files (.d.ts) are the contract: types only, no runtime.
Docs: Declaration Files, Modules — .d.ts, DefinitelyTyped.
What a declaration file is
// styles.d.ts
declare module '*.css' {
const classes: { readonly [key: string]: string };
export default classes;
}
// global.d.ts
interface Window {
__APP_CONFIG__?: { apiBase: string };
}
.d.ts files erase completely. They never emit JS. Wrong declarations = wrong confidence, not a runtime fix.
DefinitelyTyped / @types
When a package is untyped:
npm i -D @types/lodash
TypeScript looks up types via:
- The package’s own
types/typingsfield @types/<package>undernode_modules/@types- Your project’s ambient declarations
// tsconfig
{
"compilerOptions": {
"types": ["node", "vitest/globals"] // optional allowlist
}
}
Leaving "types" unset usually auto-includes all @types/* — fine for apps; lock it down if ambient pollution appears.
Built-in types vs @types conflicts
Modern packages (Zod, React 17+ packages that ship types, Vite plugins) include their own. Installing a stale @types/foo alongside shipping types can duplicate identifiers:
Duplicate identifier 'X'
Fix: remove the redundant @types package. Prefer official types when versions match.
Minimal shim for an untyped module
// types/legacy-widget.d.ts
declare module 'legacy-widget' {
export type WidgetOptions = {
el: HTMLElement;
label?: string;
};
export class Widget {
constructor(options: WidgetOptions);
destroy(): void;
setLabel(label: string): void;
}
export default Widget;
}
Start narrow: only the APIs you call. Expand as needed. Over-specified wrong types are worse than a small accurate surface.
allowJs + checkJs vs declarations
Migrating a JS codebase:
| Approach | Use when |
|---|---|
allowJs |
Import JS from TS gradually |
checkJs + JSDoc |
Type-check JS without rewrite |
Hand-written .d.ts |
Third-party or legacy without types |
Convert to .ts |
Long-term ownership |
// checked JS
/** @param {string} id */
export function loadUser(id) {
return fetch(`/api/users/${id}`);
}
Module augmentation
Extend a library’s types (e.g. theme, custom props):
// mui.d.ts or styled.d.ts
import 'styled-components';
declare module 'styled-components' {
export interface DefaultTheme {
colors: { bg: string; fg: string };
}
}
Augmentation must be a module (has import/export) to merge correctly — pure scripts behave differently.
Triple-slash references (rare)
/// <reference types="vite/client" />
Prefer tsconfig types / automatic inclusion. Triple-slash still appears in Vite client templates.
Common frontend declaration needs
// images
declare module '*.svg' {
const src: string;
export default src;
}
// ?raw / ?url loaders (bundler-specific)
declare module '*?raw' {
const content: string;
export default content;
}
// env
interface ImportMetaEnv {
readonly VITE_API_URL: string;
}
interface ImportMeta {
readonly env: ImportMetaEnv;
}
Footguns
anyin a.d.tsyou wrote — infects the whole app.- Global pollution — accidental globals without
export {}. - Wrong
export defaultvs named — match the runtime module shape. - Skipping version pin —
@typesmajor can lag or lead the library. - Editing
node_modulestypes — patch via project.d.ts+pathsorpatch-package, never rely on local edits.
// force a module (avoid global scope)
export {};
declare global {
interface Window {
analytics?: { track(event: string): void };
}
}
Interview angle
“Declaration files describe JS to TS with no runtime. DefinitelyTyped supplies @types when authors don’t ship types. I write minimal ambient modules for legacy deps, keep brands/validation at app boundaries, and avoid duplicate @types when packages already include types.”
Related on this site
- Path aliases and tooling sync
- tsconfig strict flags
- unknown vs any
- Runtime validation with schemas
- TypeScript for JavaScript engineers
Further reading
Related guides
- Basic Types and AnnotationsPrimitives, arrays, objects, and function annotations TypeScript actually checks — plus where inference is enough and where it is not.
- Branded Types for IDsNominal-style UserId vs OrderId in TypeScript — prevent ID mixups at compile time with brands, parsers, and form boundaries.
- Conditional Types IntroT extends U ? X : Y — how TypeScript picks types from conditions, distributes over unions, and powers utility types you already use.
- Discriminated Unions for UI StateModel loading, success, and error as mutually exclusive variants so TypeScript and your UI cannot show impossible states.
- Enums vs Union TypesWhen TypeScript enums help, when they hurt, and why string literal unions plus const objects win for most frontend code.