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.tsfiles 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 yourtsconfig.jsonhas"include": ["**/*.ts", "**/*.tsx"]or if the path matches anincludepattern. - 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 toProcessEnv, extend Next.js types, or patch third-party library types. - TypeScript resolves types in this order: local source files,
pathsmappings,node_modules/@types/*, then custom.d.tsfiles ininclude-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 moduleTyped 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 oneimportorexportto be treated as a module; addexport {}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 implicitany.- Do not put implementation code in
.d.tsfiles. They are erased during compilation and serve only as type hints. - Use
typeRootsintsconfig.jsonto control where TypeScript looks for declaration files:"typeRoots": ["./types", "./node_modules/@types"].
Gotchas
- Forgetting
export {}in a file withdeclare globalmakes 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: trueskips type-checking all.d.tsfiles, 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
| Approach | Pros | Cons |
|---|---|---|
Custom .d.ts files | Full control, project-specific | Must maintain manually |
@types/* packages | Community-maintained, well-tested | May lag behind library updates |
Inline declare in source files | Co-located with usage | Pollutes source files |
Zod + z.infer | Runtime validation + type inference | Not suitable for ambient/global types |
typeRoots config | Explicit type resolution order | Easy to misconfigure |
| Module augmentation | Extends existing types cleanly | Requires 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.tsfile included by yourtsconfig.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
importstatement from the module before thedeclare moduleblock 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/@typesexplicitly or lose access to DefinitelyTyped packages.
Gotcha: Can skipLibCheck: true hide errors in your own .d.ts files?
- Yes.
skipLibCheckskips type-checking all.d.tsfiles, 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 globalto modify the global scope. - The file must be a module (include
export {}).