.d.ts files and @types
The house has a device whose buttons carry no labels. You can use it, but only by guessing. A manual helps: which button does what and what the device gives back. The manual does not change the device, it only describes it.
A JavaScript library is such a device. It has functions but no types, and tsc does not know which arguments to pass to them. Let's take the popular library lodash and its function chunk, which cuts an array into parts of a given size. First the library is installed with npm, which puts packages into the project's node_modules folder:
$ npm install lodashNow import chunk:
import { chunk } from "lodash"; // ✗
console.log(chunk([1, 2, 3, 4, 5], 2));error TS7016: Could not find a declaration file for module 'lodash'. '.../node_modules/lodash/lodash.js' implicitly has an 'any' type.
Try `npm i --save-dev @types/lodash` if it exists or add a new declaration (.d.ts) file containing `declare module 'lodash';`Let's take the message apart:
tscfoundlodashin thenode_modulesfolder, but there is only JavaScript there and no description of the types. In place of the dotstscprints the full path to the project folder.- Without a description every function of the library would get the type
any, and strict mode does not allow that. - The second line suggests two ways out: install the
@types/lodashpackage or write a declaration file yourself.
The declaration file
A declaration file is a file with the .d.ts extension that holds only types: function names, their parameters and results, but not a single body. Say the project still has an old format.js in JavaScript with a function formatPrice(price). Next to it you put format.d.ts:
export declare function formatPrice(price: number): string;Let's take the line apart:
declaremeans "such a function exists, but its code is elsewhere": there is no body, and the line ends with a semicolon;exportsays that the moduleformat.jsexports the function;(price: number): stringis the very label on the button: what to pass in and what to get back.
Now main.ts writes import { formatPrice } from "./format.js" and gets the types from format.d.ts, while at run time Node.js runs format.js. The compiler stops the call formatPrice("100") with error TS2345, as if the function were written in TypeScript.
Remember lib.dom.d.ts from the first lesson of this theme? It is a declaration file too: it describes the browser's names, while their code lives in the browser itself.
@types packages
For popular libraries the declarations are already written. The community gathers them in the DefinitelyTyped repository and publishes them as separate @types/<name> packages:
$ npm install --save-dev @types/lodashWhat matters about @types packages:
tscfinds them on its own in thenode_modules/@typesfolder: the code still writesfrom "lodash", and nothing needs importing from@types/lodash;- after installing it,
chunk([1, 2, 3, 4, 5], "2")does not compile: the part size is a number, and"2"is a string; - many libraries ship their own
.d.tsfiles inside the package,axiosfor example, and they need no@typespackage; - the
--save-devflag installs the package for development only: the types are needed bytsc, not by the program at run time.
Interviewers often ask where @types packages come from and how TypeScript finds them. The answer: the community writes them in DefinitelyTyped, and tsc finds the right package in node_modules/@types on its own when the code imports the library; nothing is imported from @types.
declare module
Sometimes there are types neither in the package nor in @types. Take a small internal library of your company, tiny-slug, which turns a title into a piece of a page address. Then you write the declarations yourself, in your own .d.ts file, for example src/types.d.ts:
declare module "tiny-slug" {
export function slug(text: string): string;
}declare module "tiny-slug" describes the whole package, and the braces hold what it exports. The file sits in src and is checked together with the others, so import { slug } from "tiny-slug" gets the types, and slug(42) no longer compiles.
The TS7016 message itself suggests a short form: declare module "tiny-slug"; declares the package with no details, and everything imported from it gets the type any. It is a quick way out, but there will be no checks for that package.
Here the manual picture stops working. A real manual is written by the device's maker. Declarations can be written by anyone, and tsc trusts them: make a mistake in a .d.ts, and tsc lets a wrong call through.
Common mistake
Importing a function from the types package:
import { chunk } from "@types/lodash"; // ✗error TS6137: Cannot import type declaration files. Consider importing 'lodash' instead of '@types/lodash'.@types/lodash holds only the description, with none of the library's code. Import from the library itself, from "lodash", and tsc takes the types from @types on its own.
Key points
- A
.d.tsfile describes JavaScript that has no types: it holds only declarations withdeclare, without bodies. tscfinds@types/<name>packages from DefinitelyTyped innode_modules/@typeson its own; nothing needs importing from them.- A library that ships its own
.d.tsfiles needs no@typespackage. - For a package without types you write the declarations yourself:
declare module "name" { … }in your own.d.tsfile.