Declaration files and third-party types
Most npm packages you install aren't written in TypeScript at all — so where do the types come from when you import them? A parallel universe of .d.ts files, sometimes shipped with the package, sometimes maintained entirely separately.
4 min read
A .d.ts file describes types, with zero actual implementation
// math-helpers.d.ts
export function add(a: number, b: number): number;
export function multiply(a: number, b: number): number;A declaration file (.d.ts) contains type information only — function signatures, interfaces, type aliases — with no real function bodies, no actual runtime code at all. It exists purely to tell TypeScript's checker "here's the shape of something that exists elsewhere, already compiled or written in plain JavaScript" — the actual, real add/multiply implementation lives in a separate .js file the declaration file is describing, not replacing.
Where a package's types actually come from: three real possibilities
import { z } from "zod"; // Zod ships its own .d.ts files directly in the package
import express from "express"; // Express itself has no types — a separate package provides them
import legacyLib from "some-old-untyped-package"; // Error: Could not find a declaration fileBundled types: many modern packages (like zod) are either written in TypeScript originally or ship hand-written .d.ts files directly inside their own npm package — nothing extra to install, types just work the moment the package is imported. DefinitelyTyped: for popular packages that don't ship their own types (like express), the community maintains a separate, enormous repository of types published under the @types/ npm scope — npm install --save-dev @types/express installs a .d.ts-only package that TypeScript automatically picks up for the real express package. No types at all: a smaller or less popular package with neither its own types nor a @types/ package produces a genuine "could not find a declaration file" error, which the next section covers how to work around.
Ambient declarations: telling TypeScript about something with no .d.ts file anywhere
// global.d.ts, included in the project but not imported anywhere
declare module "some-old-untyped-package" {
export function doSomething(input: string): number;
}
declare global {
interface Window {
myAnalytics: { track(event: string): void };
}
}declare module "name" creates an ambient declaration — it tells TypeScript "trust me, a module with this exact import name exists and has this shape," without TypeScript ever verifying that against real, executable code (nothing here is actually checked against the real package's real exports; a wrong ambient declaration produces confident-looking types that are simply incorrect). declare global similarly extends genuinely global constructs — like adding a custom property to the browser's Window interface, the same declaration-merging mechanism the interfaces lesson covered, applied here specifically to describe something that exists at runtime but was never declared in any type anywhere else.
The "as any" escape hatch — and why it's a last resort specifically here
// Fastest way past a missing-types error — genuinely unsafe
const lib = require("untyped-package") as any;
lib.anyMethodAtAll(); // no error, no matter what — any's usual contagion problem, applied to a whole libraryFor a truly untyped, one-off dependency, asserting the whole import as any gets past the compile error immediately — but it's the exact "reintroduces the class of bug TypeScript exists to prevent" trade-off the any-vs-unknown lesson covered, just scoped to an entire external library's surface instead of one value. A minimal, hand-written ambient declaration (even just declaring the handful of functions actually used, not the library's full API) is almost always the better trade: real type checking on the specific surface actually used, for a genuinely small amount of extra, one-time work.
Publishing your own types, briefly
// package.json, for a package you're publishing
{
"name": "my-library",
"main": "dist/index.js",
"types": "dist/index.d.ts"
}If you ever publish a TypeScript package to npm, the "types" field in package.json (or "typings", an older equivalent) tells consumers' TypeScript compilers exactly which .d.ts file describes the package — tsc automatically generates these .d.ts files from your real .ts source when the declaration: true compiler option is set, so a project's own types and its published types stay in sync automatically, generated from the same source rather than hand-maintained separately.
Further reading
Check your understanding
A quick comprehension check — not tracked, not graded, just for you.
1. What does a `.d.ts` declaration file actually contain?
2. Where do a popular untyped npm package's types typically come from, if the package itself doesn't ship any?
3. What does `declare module "some-package"` actually guarantee is true about that module's real exports?