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/nodethat contains only declaration files for another package. - types (option)
- A tsconfig list of
@typespackages 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 isany.- 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.tsfiles.
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).
$ 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:
declare module "tiny-confetti";
// src/main.ts: const n: number = burst("three"); // no error: burst is any03Better: 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):
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:
$ 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 005Where do an import's types come from?
TypeScript looks in these places. The picture is the one to keep in mind when TS7016 appears.
Try it yourself
- 1
Find the quiet any
Add
declare const confetti: any;to the third example, then callconfetti(1, 2, 3). Predict the Types panel: nothing, becauseanyallows everything. Now changeanyto(n: number) => stringand call it with three arguments:TS2554. - 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
Who needs which types?
For a Node script that uses
expressand readsprocess.env, list the packages to install and thetypessetting. (express,@types/express,@types/node;"types": ["node"].)
Code & diagrams
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 aboveThis 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
trueYou 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# 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 0Break 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;.
Break #2
Import an untyped package
import burst from "tiny-confetti"; when the package has no types.
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
@typesversions can produce baffling errors (two copies of@types/react, for example, give "type X is not assignable to type X" on identical-looking types). Checknpm ls @types/reactwhen that happens. - ▸
Because
typesdefaults to[]in 6.0 and 7.0, upgrading a project that relied on automatic@typesinclusion yields manyTS2591andTS2593errors (Cannot find namedescribe`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
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.tsfile holds only declarations, anddeclarepromises that something exists.) Bundled types. Modern libraries are written in TypeScript or ship.d.tsfiles, and theirpackage.jsonpoints to them with atypesfield orexportsconditions (Topic 11.2). Younpm installand import; types just work. This is the best case. On npmjs.com these packages show a small "TS" badge, and the package page lists thetypesentry. - 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/expresswhen youimport "express". The version of@types/xshould match the major version ofxitself, because types describe a specific version. - 3
**The
typesoption.** Some@typespackages add globals (names usable in every file without an import, Topic 11.5) instead of module exports:@types/nodeaddsprocessandBuffer, a test-runner's types adddescribeandit. Until TypeScript 5.9, tsc included *every*@types/*package it found innode_modules, which slowed builds and caused surprising globals. **TypeScript 6.0 and 7.0 default totypes: []**: no global@typesare loaded unless you list them. Thenprocess.envgivesTS2591(real error) until you write"types": ["node"]. This affects globals only:import { readFileSync } from "node:fs"also needstypes: ["node"]because that module's declarations are part of@types/node, butimport express from "express"finds@types/expressby the normal rules. - 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@typespackage, or writedeclare module "tiny-confetti";. That one line (the shorthand) tells TypeScript the module exists and everything in it isany. It's honest and fast, and every use is unchecked: in the real test,const n: number = burst("three")passed with the shorthand. - 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 reportsTS2345and the bad assignmentTS2322). Put such blocks in a.d.tsfile in your project, for examplesrc/types/tiny-confetti.d.ts, that is included by your tsconfig. The file must be a script (no top-levelimportorexport); otherwisedeclare module "x"is treated as an *augmentation* (an addition to the types of a module that already exists) and fails withTS2664(live example below, and Topic 11.5). - 6
The same trick types non-code imports that a bundler handles:
declare module "*.svg" { const url: string; export default url; }makesimport logo from "./logo.svg"astring(real run). Without it, TypeScript saysCannot find module './logo.svg'(TS2307). Vite ships these declarations invite/client; Next.js has its own. - 7
**
skipLibCheck** tells tsc not to check the inside of any.d.tsfile, only your code against them. It is on intsc --init. It makes builds much faster and hides errors inside broken or clashing declarations innode_modules(real run: a package withexport declare const a: Missing;reportedTS2304until--skipLibCheck). The cost: it also skips errors in *your own*.d.tsfiles. It's a good default for apps; for libraries you publish, check your own declarations in CI without it.
Explain it without notes
Where can the types for an imported package come from?
What does the types compiler option control, and what changed in TypeScript 6 and 7?
Why can declare module "x"; be risky, and what is a better alternative?
What does skipLibCheck do, and what is its downside?
Practice
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.
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 moduleis 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
typeslist 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 moduleblock and say where it must live.I can say what
skipLibCheckskips.