React SME Cookbook
All FAQs

Search Documentation

Search across all documentation pages

typescriptreactdeclarationd.tsmodule-augmentationglobal-typesambient

Declaration Files

Recipe

Create and manage .d.ts declaration files for global types, module augmentation, untyped third-party packages, and custom ambient declarations in a React/Next.js project.

Working Example

// types/global.d.ts - Global type declarations
type User = {
  id: string;
  name: string;
  email: string;
  role: "admin" | "editor" | "viewer";
};
 
type ApiResponse<T> = {
  data: T;
  meta: {
    page: number;
    totalPages: number;
  };
};
// types/environment.d.ts - Typed environment variables
declare namespace NodeJS {
  interface ProcessEnv {
    NODE_ENV: "development" | "production" | "test";
    DATABASE_URL: string;
    NEXT_PUBLIC_API_URL: string;
    NEXT_PUBLIC_SITE_URL: string;
    AUTH_SECRET: string;
  }
}
// types/modules.d.ts - Declaring untyped modules
declare module "some-untyped-library" {
  export function doSomething(input: string): Promise<string>;
  export function configure(options: { verbose: boolean }): void;
}
 
// Asset imports
declare module "*.svg" {
  const content: React.FC<React.SVGProps<SVGSVGElement>>;
  export default content;
}
 
declare module "*.png" {
  const src: string;
  export default src;
}
 
declare module "*.css" {
  const classes: Record<string, string>;
  export default classes;
}

Deep Dive

How It Works

  • .d.ts files are declaration files that contain only type information, no runtime code. They tell TypeScript about types that exist at runtime but are not expressed in TypeScript source.
  • Files in a types/ directory are included automatically if your tsconfig.json has "include": ["**/*.ts", "**/*.tsx"] or if the path matches an include pattern.
  • Ambient declarations (declare) tell TypeScript "this exists at runtime, trust me." Use them for global variables, untyped modules, and environment extensions.
  • Module augmentation extends existing module types using declare module "module-name". This is how you add fields to ProcessEnv, extend Next.js types, or patch third-party library types.
  • TypeScript resolves types in this order: local source files, paths mappings, node_modules/@types/*, then custom .d.ts files in include-matched directories.

Variations

Augmenting Next.js types:

// types/next-auth.d.ts - Extending next-auth session types
import { DefaultSession } from "next-auth";
 
declare module "next-auth" {
  interface Session {
    user: {
      id: string;
      role: "admin" | "editor" | "viewer";
    } & DefaultSession["user"];
  }
}

Extending Window:

// types/window.d.ts
declare global {
  interface Window {
    analytics: {
      track: (event: string, properties?: Record<string, unknown>) => void;
      identify: (userId: string) => void;
    };
    __ENV__: Record<string, string>;
  }
}
 
export {}; // Required to make this a module

Typed CSS Modules:

// types/css-modules.d.ts
declare module "*.module.css" {
  const classes: { readonly [key: string]: string };
  export default classes;
}
 
declare module "*.module.scss" {
  const classes: { readonly [key: string]: string };
  export default classes;
}

Creating a global utility type:

// types/utils.d.ts
type Prettify<T> = {
  [K in keyof T]: T[K];
} & {};
 
type StrictOmit<T, K extends keyof T> = Pick<T, Exclude<keyof T, K>>;
 
type Nullable<T> = T | null;

TypeScript Notes

  • Declaration files with declare global { } modify the global scope. The file must have at least one import or export to be treated as a module; add export {} if needed.
  • declare module "x" augments a module. If the module already has types, your declarations are merged. If it does not, your declaration replaces the implicit any.
  • Do not put implementation code in .d.ts files. They are erased during compilation and serve only as type hints.
  • Use typeRoots in tsconfig.json to control where TypeScript looks for declaration files: "typeRoots": ["./types", "./node_modules/@types"].

Gotchas

  • Forgetting export {} in a file with declare global makes the file a script (not a module), and its declarations may not merge correctly.
  • Ambient module declarations (declare module "x") replace the module's types entirely unless you import from the module first. To augment, add an import statement.
  • @types/* packages from DefinitelyTyped can conflict with bundled types in newer package versions. Check if the library ships its own types before installing @types/.
  • skipLibCheck: true skips type-checking all .d.ts files, including yours. Errors in your custom declarations will be silently ignored.
  • Environment variable types (ProcessEnv) provide compile-time confidence but no runtime guarantee. The variable could still be missing at runtime. Always validate at startup.

Alternatives

ApproachProsCons
Custom .d.ts filesFull control, project-specificMust maintain manually
@types/* packagesCommunity-maintained, well-testedMay lag behind library updates
Inline declare in source filesCo-located with usagePollutes source files
Zod + z.inferRuntime validation + type inferenceNot suitable for ambient/global types
typeRoots configExplicit type resolution orderEasy to misconfigure
Module augmentationExtends existing types cleanlyRequires understanding of module system

FAQs

What is the purpose of a .d.ts file?
  • It contains only type information, no runtime code.
  • Tells TypeScript about types that exist at runtime but are not expressed in TypeScript source.
  • Commonly used for global types, environment variables, and untyped third-party packages.
Why do you need export {} in a file that uses declare global?
  • Without it, TypeScript treats the file as a script, not a module.
  • Script-scoped declarations may not merge correctly with other module-scoped types.
  • Adding export {} (even with no actual exports) forces the file to be treated as a module.
How do you type environment variables in a Next.js project?
// types/environment.d.ts
declare namespace NodeJS {
  interface ProcessEnv {
    NODE_ENV: "development" | "production" | "test";
    DATABASE_URL: string;
    NEXT_PUBLIC_API_URL: string;
  }
}
  • This provides compile-time confidence but no runtime guarantee.
  • Always validate environment variables at application startup.
What is the difference between ambient declarations and module augmentation?
  • Ambient declarations (declare) tell TypeScript something exists at runtime without providing implementation.
  • Module augmentation (declare module "x") extends or replaces types of an existing module.
  • To augment (not replace), you must import from the module first before declaring.
How do you declare types for an untyped third-party library?
// types/modules.d.ts
declare module "some-untyped-library" {
  export function doSomething(input: string): Promise<string>;
  export function configure(options: { verbose: boolean }): void;
}
  • Place the declaration in a .d.ts file included by your tsconfig.json.
  • The module name must match the import path exactly.
Gotcha: What happens if you use declare module "x" without importing from that module first?
  • Your declarations replace the module's types entirely instead of augmenting them.
  • Existing types from the package are lost.
  • Add an import statement from the module before the declare module block to merge instead of replace.
How do you type SVG and image imports in a React project?
declare module "*.svg" {
  const content: React.FC<React.SVGProps<SVGSVGElement>>;
  export default content;
}
 
declare module "*.png" {
  const src: string;
  export default src;
}
  • These declarations tell TypeScript what the default export of each asset type looks like.
What does typeRoots do in tsconfig.json?
  • Controls where TypeScript looks for declaration files.
  • Example: "typeRoots": ["./types", "./node_modules/@types"].
  • Easy to misconfigure -- if you set it, you must include node_modules/@types explicitly or lose access to DefinitelyTyped packages.
Gotcha: Can skipLibCheck: true hide errors in your own .d.ts files?
  • Yes. skipLibCheck skips type-checking all .d.ts files, including your custom declarations.
  • Errors in your types/ directory will be silently ignored.
  • It is still recommended for build speed, but be aware of this trade-off.
When should you install @types/* packages vs writing your own declarations?
  • Use @types/* when the library does not ship its own types and a community package exists.
  • Write your own when no @types/* exists, or when you need project-specific overrides.
  • Check if the library already ships types before installing @types/ -- duplicates can cause conflicts.
How do you extend the Window object with custom properties in TypeScript?
// types/window.d.ts
declare global {
  interface Window {
    analytics: {
      track: (event: string) => void;
    };
  }
}
export {};
  • Use declare global to modify the global scope.
  • The file must be a module (include export {}).