Skip to content
Theory · A TypeScript project · lesson 7 of 10

.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:

TEXT
$ npm install lodash

Now import chunk:

TypeScript
import { chunk } from "lodash"; // ✗

console.log(chunk([1, 2, 3, 4, 5], 2));
TEXT
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:

  1. tsc found lodash in the node_modules folder, but there is only JavaScript there and no description of the types. In place of the dots tsc prints the full path to the project folder.
  2. Without a description every function of the library would get the type any, and strict mode does not allow that.
  3. The second line suggests two ways out: install the @types/lodash package 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:

TypeScript
export declare function formatPrice(price: number): string;

Let's take the line apart:

  • declare means "such a function exists, but its code is elsewhere": there is no body, and the line ends with a semicolon;
  • export says that the module format.js exports the function;
  • (price: number): string is 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:

TEXT
$ npm install --save-dev @types/lodash

What matters about @types packages:

  • tsc finds them on its own in the node_modules/@types folder: the code still writes from "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.ts files inside the package, axios for example, and they need no @types package;
  • the --save-dev flag installs the package for development only: the types are needed by tsc, 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:

TypeScript
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:

TypeScript
import { chunk } from "@types/lodash"; // ✗
TEXT
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.ts file describes JavaScript that has no types: it holds only declarations with declare, without bodies.
  • tsc finds @types/<name> packages from DefinitelyTyped in node_modules/@types on its own; nothing needs importing from them.
  • A library that ships its own .d.ts files needs no @types package.
  • For a package without types you write the declarations yourself: declare module "name" { … } in your own .d.ts file.