TypeScript by Example

Declaration Files

Writing .d.ts files, augmenting global types, @types/ version skew, and generating declarations from OpenAPI or GraphQL schemas instead of handwriting them.

A declaration file (.d.ts) describes the shape of a JavaScript module to TypeScript without containing any runtime code. It is the bridge between the untyped JS ecosystem and TypeScript's type system.

A .d.ts file declares types for a module. The declarations are erased at compile time - they exist only for the type checker. If a package ships without types, you write (or install) a .d.ts to tell TypeScript what the package exports.

// types/untyped-logger.d.ts
// Describes a hypothetical "untyped-logger" npm package that has no @types/ package
 
declare module "untyped-logger" {
  export interface LogLevel {
    level: "debug" | "info" | "warn" | "error";
  }
 
  export interface Logger {
    debug(msg: string, meta?: Record<string, unknown>): void;
    info(msg: string, meta?: Record<string, unknown>): void;
    warn(msg: string, meta?: Record<string, unknown>): void;
    error(msg: string, error?: Error, meta?: Record<string, unknown>): void;
  }
 
  export function createLogger(name: string): Logger;
  export const rootLogger: Logger;
}
// Now this import is fully typed
import { createLogger } from "untyped-logger";
const log = createLogger("api");
log.info("server started", { port: 3000 });

The /// <reference types="..." /> directive adds type declarations from a package to the global scope. Global augmentation extends built-in types - useful for adding custom properties to window, process.env, or Express's Request.

// global.d.ts - augment Express Request with your auth context
import "express";
 
declare global {
  namespace Express {
    interface Request {
      user?: {
        id: string;
        role: "admin" | "member";
      };
    }
  }
}
 
// env.d.ts - type process.env entries so missing vars are a type error
declare global {
  namespace NodeJS {
    interface ProcessEnv {
      DATABASE_URL: string;
      JWT_SECRET: string;
      NODE_ENV: "development" | "production" | "test";
      PORT?: string; // optional - has a default
    }
  }
}
// reference directive - include a types package in this file's scope
/// <reference types="node" />
 
const buf = Buffer.from("hello"); // Buffer is available because of the reference

@types/ packages (from DefinitelyTyped) provide types for untyped npm packages. They are versioned independently of the library - a library patch release can silently break under an old @types version or vice versa. Lock both versions in package.json.

// package.json - lock library and @types/ versions together
{
  "dependencies": {
    "express": "4.18.2"   // runtime library
  },
  "devDependencies": {
    "@types/express": "4.17.21"  // lock major.minor to match library
    // A mismatch - e.g., @types/express@5.x with express@4.x - causes
    // type errors that are invisible until you update and break everything.
  }
}
// Check if a package ships its own types (no @types/ needed):
// Look for "types" or "typings" in the package's package.json.
// If present, @types/ is unnecessary and may conflict.
import type { RequestHandler } from "express"; // from @types/express

For packages you own, TypeScript generates .d.ts files automatically from source when declaration: true is set in tsconfig.json. For API clients, generate types from your OpenAPI or GraphQL schema - two handwritten sources of truth for the same contract always diverge.

// tsconfig.json - emit declarations alongside compiled JS
{
  "compilerOptions": {
    "declaration": true,        // emit .d.ts for every .ts file
    "declarationMap": true,     // emit .d.ts.map for Go-to-Definition in source
    "declarationDir": "dist/types", // output declarations to a separate folder
    "outDir": "dist"
  }
}
OUTPUT
# Generate TypeScript types from an OpenAPI spec (openapi-typescript)
npx openapi-typescript openapi.yaml -o src/types/api.d.ts
 
# Generate TypeScript types from a GraphQL schema (graphql-code-generator)
npx graphql-codegen --config codegen.yml
// Import the generated types - no manual maintenance
import type { components } from "@/types/api";
 
type User = components["schemas"]["User"];
type CreateUserRequest = components["schemas"]["CreateUserRequest"];

In production

@types/ packages are maintained separately from the library they describe - a patch release of the library can quietly break under an old @types version, or a new @types version can introduce types incompatible with the runtime behavior you rely on. Lock both express and @types/express to matching major.minor versions in package.json. For teams that own their API: generate .d.ts files from an OpenAPI spec or GraphQL schema rather than handwriting them. Two sources of truth for the same contract always diverge - the spec diverges from the implementation, and the handwritten types diverge from the spec. A generated type file eliminates one of those gaps at zero maintenance cost.

Enjoyed this? Get more essays on software craft delivered to your inbox.

Subscribe free