Step 1 - Declare an untyped module
Suppose a package exists at runtime but has no types:
import { rankDocuments } from "legacy-ranker";
Add types/legacy-ranker.d.ts:
declare module "legacy-ranker" {
export type RankedDocument = {
id: string;
score: number;
};
export function rankDocuments(query: string, documents: string[]): RankedDocument[];
}
Now TypeScript understands the import. You still need tests or runtime checks to prove the package actually exports that function.
Step 2 - Declare asset imports
Modern bundlers often let you import files that TypeScript does not understand by default:
import schemaText from "./schema.graphql";
Add types/assets.d.ts:
declare module "*.graphql" {
const source: string;
export default source;
}
This tells TypeScript what the bundler provides at runtime.
Step 3 - Understand module vs global declarations
This file is a module because it exports something:
export declare function parse(input: string): unknown;
Its declarations must be imported.
This file is global because it has no top-level import or export:
declare function trackEvent(name: string): void;
That function appears everywhere in the TypeScript program. Use this pattern only when the runtime really creates a global.
Step 4 - Add a global safely from a module
Sometimes you need imports and a global patch. Use declare global:
import type { AnalyticsClient } from "./analytics-client";
declare global {
interface Window {
analytics?: AnalyticsClient;
}
}
export {};
The empty export keeps the file in module mode. The declare global block is the explicit escape hatch.
Step 5 - Avoid global conflicts
Global declarations can collide with DOM types, Node types, test-runner types, and runtime types from other packages.
Prefer this:
declare module "my-runtime" {
export function file(path: string): Promise<string>;
}
Over this:
declare function file(path: string): Promise<string>;
The module version requires an import and stays scoped. The global version changes the whole program.
Step 6 - Make tsconfig include your declarations
Most projects include .d.ts files automatically through include, but verify it:
{
"include": ["src", "types"]
}
If TypeScript still cannot find the declaration, check:
- the file extension is
.d.ts; - the file is inside
include; exclude is not removing it;- the module name exactly matches the import string;
- the declaration file does not accidentally become global or module scoped in the wrong way.
Step 7 - Treat declarations as contracts
Ambient declarations can make broken code typecheck. This is the danger.
If you declare:
declare module "runtime-flags" {
export function isEnabled(flag: string): boolean;
}
but the real package returns a promise, TypeScript will trust your wrong declaration. Your app will fail later.
Use declaration files with the same seriousness as API docs:
- keep them small;
- link to runtime source when possible;
- test the runtime behavior;
- avoid broad
any; - update declarations when the runtime changes.