Ch. 2 · TypeScript

TypeScript Declaration Files and Untyped Packages

Write .d.ts files for JavaScript packages, declare modules, and expose types with the package.json types field.

~2 min readadvancedupdated Oct 5, 2026

A declaration file (.d.ts) describes the types of JavaScript that has no type definitions, so TypeScript can check code that calls it. Declaration files contain types only: no runtime implementation, no executable statements.

Before you start

You should be comfortable with modules and ambient declarations. This article covers local declarations for untyped packages; publishing a typed library is a related but larger task.

Step-by-step walkthrough

Step 1: Check for existing types first

Before writing anything, look for a bundled types field or an @types/<package> package. Hand-written declarations drift from the real runtime, so use the maintained ones when they exist and only author your own when they do not.

Step 2: Declare the module surface

Use declare module 'legacy-lib' { ... } to describe exports. Each export function, export const or export default mirrors the runtime shape. For a global script that is not a module, use ambient declarations without the declare module wrapper.

Step 3: Make the file discoverable

Place the file where tsconfig.json includes it (often a types/ folder added to include) or reference it with /// <reference path="..." />. For a package you publish, add "types": "./dist/index.d.ts" to package.json so consumers resolve it automatically.

Worked scenario

The declaration gives an untyped package a named module surface.

// types/legacy-lib.d.ts
declare module 'legacy-lib' {
  export interface Options {
    strict?: boolean;
  }
  export function parse(input: string, options?: Options): { ok: boolean };
}
TypeScript

Walk through the example

The file declares only types: an Options interface and a parse function signature. Code that imports legacy-lib is now checked, so passing a number to parse is an error. The declaration does not change runtime behavior; if the real package’s signature differs, the type is simply wrong, which is why keeping it in sync matters.

Common mistake

Putting implementation or executable code in a .d.ts file, which is ignored or errors. Another is adding any to every signature just to silence the compiler, which produces no safety. A missing include entry means the file exists but the compiler never sees it.

Verify the behavior

Import the package in a .ts file and confirm it resolves and type-checks. Pass an argument of the wrong type and confirm the error. If the declaration is not seen, check tsconfig.json include/files and the types field. Try a missing export and confirm the compiler reports the unknown member.

Interview exercise

A package ships no types and there is no @types package. What are the options, and which is safest?

Answer and reasoning

Options are: write a local .d.ts declaration, contribute types to DefinitelyTyped, or wrap the package in a thin typed adapter. The safest low-effort choice is a local .d.ts scoped to the parts you use, kept minimal to limit drift. A typed adapter adds runtime code but centralizes the boundary, which is better when only a few functions are needed.

Continue learning

See module syntax in module type imports and ambient declarations in declaration merging. Read the TypeScript declaration files primer and try the TypeScript interview questions.

More in TypeScript

read ✓TypeScript · hard

TypeScript Abstract Classes and Contracts

Share behavior with abstract classes, enforce required members, and decide when an interface or composition is the better contract.

~2 min readread →
read ✓TypeScript · mid

TypeScript Enums vs Union Types

Compare enums with unions of string literals: runtime cost, serialization, exhaustiveness and which one fits application code.

~3 min readread →
esc