Command Palette

Search for a command to run...

Hectal
PHASE 11Intermediate ~21 min· topic 4 of 6

Topic 11.4

Typing Third-Party Code

In one line

Most npm packages are JavaScript, so TypeScript needs their types from somewhere: bundled inside the package, from a @types/* package, or from a short declare module you write yourself. Since TypeScript 6 you also choose which global @types load, with the types option.

Think of it like this

You buy furniture from a shop. Some boxes come with the assembly leaflet inside (the package bundles its types). For other brands the leaflet is sold separately by a helpful neighbour, who has made one for thousands of brands (DefinitelyTyped, the @types/* packages). And if there is no leaflet at all, you can scribble a quick sketch yourself (a declare module). In all three cases, the furniture (the JavaScript) is the same. Only the leaflet (the types) differs.

Words you'll meet

New words in this topic, in plain English. Come back here whenever one feels fuzzy.

Bundled types
Type files that come inside the package itself.
DefinitelyTyped
The community repository of type files for packages that don't ship their own.
@types package
An npm package such as @types/node that contains only declaration files for another package.
types (option)
A tsconfig list of @types packages whose globals are loaded automatically. Empty by default in TypeScript 6 and 7.
Shorthand ambient module
declare module "x"; with no body: the module exists and everything in it is any.
TS7016
The error for an import whose JavaScript has no declaration file.
Wildcard module
declare module "*.svg" describes every import that matches the pattern.
skipLibCheck
A setting that skips checking inside .d.ts files.

Step by step

01An untyped package

The package tiny-confetti is plain JavaScript with no types field and no @types/tiny-confetti. Importing it fails with TS7016 (real output from tsc 7.0.2; <project> stands for the long folder path in the real message).

src/main.ts + terminalwhole filebash
$ cat src/main.ts
import burst from "tiny-confetti";

console.log(burst(3));

$ tsc -p .
src/main.ts(1,19): error TS7016: Could not find a declaration file for module 'tiny-confetti'. '<project>/node_modules/tiny-confetti/index.js' implicitly has an 'any' type.
  Try `npm i --save-dev @types/tiny-confetti` if it exists or add a new declaration (.d.ts) file containing `declare module 'tiny-confetti';`

02Quick fix: the one-line shorthand

Create src/untyped.d.ts with one line. The error goes away (exit 0). Now every use of burst is any, so this compiles, which is exactly the danger:

src/untyped.d.tswhole filets
declare module "tiny-confetti";

// src/main.ts:   const n: number = burst("three");   // no error: burst is any

03Better: type what you use

Replace the shorthand with a real description. It doesn't have to describe the whole library, only the parts you call. Now the same bad line is rejected (real output):

src/untyped.d.tswhole filets
declare module "tiny-confetti" {
  export default function burst(count: number): string;
}

// src/main.ts(3,7): error TS2322: Type 'string' is not assignable to type 'number'.
// src/main.ts(3,25): error TS2345: Argument of type 'string' is not assignable to parameter of type 'number'.

04Global types need types

With no types option, TypeScript 7.0.2 doesn't load @types/node, even though it is installed:

terminalwhole filebash
$ cat src/main.ts
const home = process.env.HOME;
console.log(home);

$ tsc -p .
src/main.ts(1,14): error TS2591: Cannot find name 'process'. Do you need to install type definitions for node? Try `npm i --save-dev @types/node` and then add 'node' to the types field in your tsconfig.

# add to compilerOptions:  "types": ["node"]
$ tsc -p . ; echo "exit $?"
exit 0

05Where do an import's types come from?

TypeScript looks in these places. The picture is the one to keep in mind when TS7016 appears.

Where do an import's types come from?diagram
Rendering diagram…

Try it yourself

  1. 1

    Find the quiet any

    Add declare const confetti: any; to the third example, then call confetti(1, 2, 3). Predict the Types panel: nothing, because any allows everything. Now change any to (n: number) => string and call it with three arguments: TS2554.

  2. 2

    Say why

    Read the TS2664 message in the first example aloud. Why does a module file think "tiny-confetti" should already exist? (Because in a module, declare module "x" extends x. Topic 11.5.)

  3. 3

    Who needs which types?

    For a Node script that uses express and reads process.env, list the packages to install and the types setting. (express, @types/express, @types/node; "types": ["node"].)

Code & diagrams

declare module belongs in a script, not in a module file

This editor file is a module (the editor forces it), so declare module is read as an augmentation of an existing module, and there is none. In a real project put these lines in a src/types/*.d.ts file without any import or export and they work.

Console

(nothing was printed)

Expected console output

module-shaped declarations above
types: [] means no Node globals

This editor uses types: [] like TypeScript 6 and 7 do. @types/node is not loaded, so process is unknown to tsc. (In a browser it really is undefined, so the check prints true.) Node projects add "types": ["node"].

Console

(nothing was printed)

Expected console output

true
What a hand-written typing looks like

You describe the shape of the thing once. The compiler then checks every use of prettyBytes. Nothing here runs: there is no real prettyBytes, so we never call it.

Console

(nothing was printed)

Expected console output

only the types exist here
Project: assets and skipLibCheck (real tsc 7.0.2)bash
# importing an image with no declaration
src/main.ts(1,18): error TS2307: Cannot find module './logo.svg' or its corresponding type declarations.

# src/assets.d.ts
declare module "*.svg" {
  const url: string;
  export default url;
}
# now  const url: number = logo;  reports:
src/main.ts(3,7): error TS2322: Type 'string' is not assignable to type 'number'.

# a package whose own index.d.ts contains  export declare const a: Missing;
$ tsc -p .
node_modules/bad/index.d.ts(1,25): error TS2304: Cannot find name 'Missing'.
$ tsc -p . --skipLibCheck ; echo "exit $?"
exit 0

Break it on purpose

Errors are the best teachers. Make each change, read the error, guess what went wrong, then reveal the answer.

Break #1

Use process without the Node types

In a project with no types option, write const home = process.env.HOME;.

terminal
$ tsc -p .
── what you'll see ──
src/main.ts(1,14): error TS2591: Cannot find name 'process'. Do you need to install type definitions for node? Try `npm i --save-dev @types/node` and then add 'node' to the types field in your tsconfig.

Break #2

Import an untyped package

import burst from "tiny-confetti"; when the package has no types.

terminal
$ tsc -p .
── what you'll see ──
src/main.ts(1,19): error TS7016: Could not find a declaration file for module 'tiny-confetti'. '<project>/node_modules/tiny-confetti/index.js' implicitly has an 'any' type.

Myth vs fact

Myth

@types/x contains the library's code.

Fact

It holds only type declarations. You still need x itself installed.

Myth

skipLibCheck skips type-checking the libraries' code.

Fact

It skips checking the contents of .d.ts files (yours included). Your own .ts code is still fully checked against those declarations.

Myth

The shorthand declare module "x"; is harmless.

Fact

It silences the error by making everything from that module any, so mistakes in how you use it go unnoticed. Use it as a stopgap and replace it with real types.

Pro corner

Extra depth for experienced readers. New to this? Skip it for now and come back later.

  • ▸

    Duplicate or mismatched @types versions can produce baffling errors (two copies of @types/react, for example, give "type X is not assignable to type X" on identical-looking types). Check npm ls @types/react when that happens.

  • ▸

    Because types defaults to [] in 6.0 and 7.0, upgrading a project that relied on automatic @types inclusion yields many TS2591 and TS2593 errors (Cannot find name describe` is TS2593). The fix is mechanical: list what you need ("types": ["node", "vitest/globals"]). It also makes builds faster, since tsc no longer parses every @types` package it can find.

  • ▸

    A package that ships types must make sure they are correct for both ESM and CJS importers. Tools such as "Are the types wrong?" check this by running the same resolution TypeScript does. It is worth a CI step for libraries.

Remember this

  1. 1

    The problem this solves: most npm packages are plain JavaScript, so without types every call to them is unchecked. There are three ways to get types, and this topic shows when to use each. (Recall from Topic 11.3: a .d.ts file holds only declarations, and declare promises that something exists.) Bundled types. Modern libraries are written in TypeScript or ship .d.ts files, and their package.json points to them with a types field or exports conditions (Topic 11.2). You npm install and import; types just work. This is the best case. On npmjs.com these packages show a small "TS" badge, and the package page lists the types entry.

  2. 2

    DefinitelyTyped. For popular JavaScript libraries that don't ship types, volunteers write them in one big repository, published to npm as **@types/<name>**: @types/node, @types/express, @types/react. You install them as development dependencies (npm i -D @types/express). TypeScript finds @types/express when you import "express". The version of @types/x should match the major version of x itself, because types describe a specific version.

  3. 3

    **The types option.** Some @types packages add globals (names usable in every file without an import, Topic 11.5) instead of module exports: @types/node adds process and Buffer, a test-runner's types add describe and it. Until TypeScript 5.9, tsc included *every* @types/* package it found in node_modules, which slowed builds and caused surprising globals. **TypeScript 6.0 and 7.0 default to types: []**: no global @types are loaded unless you list them. Then process.env gives TS2591 (real error) until you write "types": ["node"]. This affects globals only: import { readFileSync } from "node:fs" also needs types: ["node"] because that module's declarations are part of @types/node, but import express from "express" finds @types/express by the normal rules.

  4. 4

    No types at all. If a package has neither bundled types nor @types, import x from "tiny-confetti" is TS7016: *Could not find a declaration file for module*. The message suggests two things: look for an @types package, or write declare module "tiny-confetti";. That one line (the shorthand) tells TypeScript the module exists and everything in it is any. It's honest and fast, and every use is unchecked: in the real test, const n: number = burst("three") passed with the shorthand.

  5. 5

    Better: describe only what you use. A declare module "tiny-confetti" { export default function burst(count: number): string; } block gives real checking (real run: the bad call now reports TS2345 and the bad assignment TS2322). Put such blocks in a .d.ts file in your project, for example src/types/tiny-confetti.d.ts, that is included by your tsconfig. The file must be a script (no top-level import or export); otherwise declare module "x" is treated as an *augmentation* (an addition to the types of a module that already exists) and fails with TS2664 (live example below, and Topic 11.5).

  6. 6

    The same trick types non-code imports that a bundler handles: declare module "*.svg" { const url: string; export default url; } makes import logo from "./logo.svg" a string (real run). Without it, TypeScript says Cannot find module './logo.svg' (TS2307). Vite ships these declarations in vite/client; Next.js has its own.

  7. 7

    **skipLibCheck** tells tsc not to check the inside of any .d.ts file, only your code against them. It is on in tsc --init. It makes builds much faster and hides errors inside broken or clashing declarations in node_modules (real run: a package with export declare const a: Missing; reported TS2304 until --skipLibCheck). The cost: it also skips errors in *your own* .d.ts files. It's a good default for apps; for libraries you publish, check your own declarations in CI without it.

Explain it without notes

01

Where can the types for an imported package come from?

02

What does the types compiler option control, and what changed in TypeScript 6 and 7?

03

Why can declare module "x"; be risky, and what is a better alternative?

04

What does skipLibCheck do, and what is its downside?

Practice

01

Your app imports import slug from "url-slug-lite", which has no types. Write a src/types/url-slug-lite.d.ts that types slug(text: string, options?: { separator?: string }): string as the default export.

02

You upgraded to TypeScript 7 and a test file now says Cannot find name 'describe'. What is the cause and the fix, in terms of tsconfig?

Trade-offs

  • ↔

    Writing your own small declare module is quick and typed exactly as you use it, but you maintain it. A DefinitelyTyped package is maintained by others and more complete, but may lag the library or be missing.

  • ↔

    An explicit types list is more typing in tsconfig, but your build only loads what you mean, and global names can't appear from a forgotten dependency.

Done when you can

  • I can name the three places an import's types can come from.

  • I know when to install an @types/* package and which version to match.

  • I can explain types: [] and fix TS2591 for Node.

  • I can write a short declare module block and say where it must live.

  • I can say what skipLibCheck skips.