How to Create Type Definitions in TypeScript (.d.ts)

We have all experienced this moment in TypeScript. You install a useful npm package, import it into your code, and the TypeScript compiler immediately stops you with an error:
Could not find a declaration file for module 'old-library'.
'node_modules/old-library/index.js' implicitly has an 'any' type.
Try `npm i --save-dev @types/old-library` if it exists.You try to run npm i -D @types/old-library, but npm tells you the package does not exist on DefinitelyTyped.
Many developers respond by adding // @ts-ignore or casting the import to any. That removes the compiler warning, but it destroys type safety and autocompletion for your entire team.
The clean solution is to write your own type definition file (.d.ts). In this post, I will show you how to declare types for untyped npm packages, configure tsconfig.json, and augment global objects like window and process.env.
1. What Are Declaration Files (.d.ts)?
TypeScript separates executable logic from type descriptions. Standard .ts files compile to .js files. In contrast, .d.ts files contain only type metadata:
- They never generate JavaScript output.
- They describe function signatures, class shapes, and object properties.
- They use the
declarekeyword to inform TypeScript that the real implementation exists elsewhere at runtime.
Before writing complete signatures, let us look at the fastest way to unblock your build.
2. Quick Fix: Shorthand Ambient Declarations
If you need your project to build immediately, you can create a shorthand ambient module declaration.
Create a folder in your project named src/types/ and add a file named untyped-modules.d.ts:
// src/types/untyped-modules.d.ts
declare module 'old-library';This single line tells TypeScript: "Trust me, 'old-library' exists at runtime. Treat anything imported from it as any."
import { calculateTotal } from 'old-library';
// calculateTotal is typed as any, but the build succeeds
const result = calculateTotal(10, 20);This shorthand fixes the compiler error instantly. However, you do not get autocompletion or parameter safety. To get full type safety, we need to write specific type definitions.
3. Writing Complete Module Declarations
Let us imagine we have an untyped HTTP utility package named simple-fetcher.
The library exposes a client object with get, post, and custom request options. We want full autocompletion in our editor.
Create a file named src/types/simple-fetcher.d.ts:
// src/types/simple-fetcher.d.ts
declare module 'simple-fetcher' {
export interface RequestOptions {
timeout?: number;
headers?: Record<string, string>;
}
export interface ApiResponse<T> {
data: T;
status: number;
ok: boolean;
}
export function get<T = unknown>(
url: string,
options?: RequestOptions
): Promise<ApiResponse<T>>;
export function post<T = unknown>(
url: string,
body: unknown,
options?: RequestOptions
): Promise<ApiResponse<T>>;
}Every exported item matches the runtime JavaScript behavior of the package. Now when we consume the library, our editor provides autocomplete and strict parameter checking:
import { get, RequestOptions } from 'simple-fetcher';
interface User {
id: string;
name: string;
}
const config: RequestOptions = { timeout: 5000 };
// TypeScript knows response.data is User!
const response = await get<User>('https://api.example.com/user/1', config);
console.log(response.data.name);If you pass an invalid option (like timeout: "fast"), the compiler catches the error immediately before your code reaches production.
Now that our type file is written, we must make sure the TypeScript compiler actually loads it.
4. Configuring tsconfig.json to Include Local Types
By default, TypeScript looks for type definitions inside node_modules/@types.
To make TypeScript recognize your custom .d.ts files, update your tsconfig.json:
{
"compilerOptions": {
"target": "ES2022",
"moduleResolution": "bundler",
"strict": true,
"typeRoots": [
"./node_modules/@types",
"./src/types"
]
},
"include": [
"src/**/*",
"src/types/**/*.d.ts"
]
}There are two important settings here:
typeRoots: Tells TypeScript which directories contain ambient type definitions. Always keep./node_modules/@typesso your installed npm types keep working.include: Ensures the compiler includes your.d.tsfiles during compilation.
With tsconfig.json configured, your custom definitions apply across the entire project.
Next, what happens when a package already has types, but you need to add custom methods? That requires module augmentation.
5. Module Augmentation: Extending Existing Libraries
Sometimes an existing library does have types, but your application adds a plugin or middleware that attaches new properties to its objects.
For example, imagine you use Express or Fastify, and your auth middleware attaches a currentUser to the request object.
You can augment the library's types inside a .d.ts file:
// src/types/express-augmented.d.ts
import 'express';
declare module 'express-serve-static-core' {
interface Request {
currentUser?: {
id: string;
email: string;
role: 'admin' | 'member';
};
}
}Notice the import 'express' statement at the top. This import turns the file into an external module. This tells TypeScript to merge your new currentUser property with the existing Request interface rather than replacing it.
In your route handlers, the new property is now recognized:
import { Request, Response } from 'express';
export function profileHandler(req: Request, res: Response) {
if (!req.currentUser) {
return res.status(401).send('Unauthorized');
}
// TypeScript knows currentUser has id, email, and role!
res.json({ userId: req.currentUser.id, role: req.currentUser.role });
}This same pattern works for extending global JavaScript objects like window or process.env.
6. Extending Global Objects (window and process.env)
In browser and Node.js environments, we frequently need to access environment variables or third-party scripts attached to the global scope.
Adding Custom Properties to the Browser window
If you embed an analytics script or payment SDK (like Google Tag Manager or Stripe), it attaches to window:
// src/types/global.d.ts
export {};
declare global {
interface Window {
dataLayer: Array<Record<string, unknown>>;
analyticsScriptLoaded?: boolean;
}
}The export {}; at the top ensures TypeScript treats the file as a module. Inside declare global, any interface you declare merges with the standard browser DOM types.
Now you can write window.dataLayer.push(...) without any TypeScript complaints.
Typing Node.js process.env
Instead of seeing string | undefined everywhere you access environment variables, you can type process.env:
// src/types/environment.d.ts
export {};
declare global {
namespace NodeJS {
interface ProcessEnv {
NODE_ENV: 'development' | 'production' | 'test';
DATABASE_URL: string;
PORT?: string;
API_SECRET_KEY: string;
}
}
}Now whenever you read process.env.DATABASE_URL, TypeScript knows it exists and autocompletes the valid keys.
7. Comparison: When to Use Each Type Declaration Technique
Here is a quick reference table to help you pick the right technique:
| Approach | File Location | When to Use | Resulting Type Safety |
|---|---|---|---|
| Shorthand Ambient | src/types/*.d.ts | Quick fix for unblocking CI/CD builds | Low (any) |
| Complete Module | src/types/*.d.ts | Untyped npm packages without DefinitelyTyped | High (strict types) |
| Module Augmentation | src/types/*.d.ts | Adding plugins/middleware to typed libraries | High (interface merging) |
| Global Augmentation | src/types/*.d.ts | Typing window, process.env, or global scripts | High (global scope) |
Conclusion & Next Steps
Writing custom .d.ts declaration files is one of the most practical skills you can have in TypeScript:
- Avoid
anyand@ts-ignore: A short declaration file takes five minutes to write and protects your whole codebase. - Start simple: Use shorthand ambient declarations to unblock the build, then add explicit functions and interfaces as needed.
- Use augmentation: Extend existing third-party interfaces and global objects using
declare moduleanddeclare global.
If you want to continue strengthening your TypeScript architecture, take a look at these related guides:
Mastering the language of the web. From basic types to advanced generics and patterns.
Frequently Asked Questions
What is a .d.ts file in TypeScript?
A .d.ts file is a declaration file containing only type information without runtime implementation. It teaches the TypeScript compiler about the shapes, functions, and classes of JavaScript code.
How do I fix 'Could not find a declaration file for module' error?
First check if a community package exists with npm install -D @types/package-name. If not, create a local declaration file with declare module 'package-name' and add its path to your tsconfig.json.
What is module augmentation in TypeScript?
Module augmentation allows you to extend existing type definitions from third-party libraries or internal modules without modifying their original source files.
How do I add custom properties to window or process.env?
You can use declare global in a .d.ts file to extend the Window interface or the NodeJS.ProcessEnv interface with your custom variables.
Related Articles
Why You Should Avoid Using 'any' in TypeScript
Why using any in TypeScript leads to hidden runtime bugs, and how to replace it with unknown, custom type guards, utility types, and the satisfies operator.
3 Ways of Type Transformation in Typescript
When we use types or interfaces, the typescript compiler enforces the object fit with them to avoid runtime errors for missing fields. Sometimes we want the flexibility to create an object without breaking the contract with the interface type. For ex...
How to Check Types in TypeScript (The Complete Guide)
Learn how to check and validate types in TypeScript at compile-time and runtime using typeof, instanceof, the in operator, discriminated unions, custom type guards, and Zod or Valibot.
Combining Types and Interfaces with & and | in TypeScript
Learn how to compose TypeScript types using Union (|), Intersection (&), and interface extends with practical examples, decision matrices, and real-world patterns.
Share this article
If you found this guide helpful, consider sharing it with your team or fellow developers.
Real Software. Real Lessons.
I share the lessons I learned the hard way, so you can either avoid them or be ready when they happen.
Join 13,800+ developers and readers.
No spam ever. Unsubscribe at any time.