ESC

Type to search the knowledge base.

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.

intermediate3 min read
  • 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:

  1. The package’s own types / typings field
  2. @types/<package> under node_modules/@types
  3. 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

  1. any in a .d.ts you wrote — infects the whole app.
  2. Global pollution — accidental globals without export {}.
  3. Wrong export default vs named — match the runtime module shape.
  4. Skipping version pin — @types major can lag or lead the library.
  5. Editing node_modules types — patch via project .d.ts + paths or patch-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.”

Further reading

Related guides