Declaration files (.d.ts)
See what a .d.ts file holds, where @types packages come from, how to describe an untyped JavaScript library, and how tsc writes declarations for you.
- 01Read a declaration fileSay what a
.d.tsfile holds, why it has no code that runs, and how TypeScript uses it to check yours. - 02Get types for a packageFix
Could not find a declaration filewith an@typespackage from DefinitelyTyped, or with a declaration of your own. - 03Write and generate declarationsDescribe a JavaScript library with
declare, and have tsc write.d.tsfiles withdeclaration: true.
Types for JavaScript that has none
Much of the code a frontend app uses is plain JavaScript: an older file in your own project, or a package from npm. Plain JavaScript has no types. Here is a small library that formats prices:
export function formatPrice(price, currency = "$") {
return currency + price.toFixed(2);
}Lines 1 to 3 export formatPrice. It takes a price and a currency sign, "$" unless you pass another, and gives back text like $3.00. Nothing says that the price must be a number, or that the result is text. Here is a TypeScript file that uses it:
import { formatPrice } from "./price-format"; const label = formatPrice(3);// ^?console.log(label);Line 1 imports formatPrice, and TypeScript marks it on purpose: Could not find a declaration file for module './price-format'. The rest of the message, implicitly has an 'any' type, means "I don't know its types, so I'll allow anything". The query on line 4 shows it: label is any, the type that switches checking off. The code still runs and prints $3.00.
The fix is a declaration file: a file whose name ends in .d.ts and that holds only types. Put one beside the JavaScript file, with the same name:
// Types for price-format.js, which has none of its own.export declare function formatPrice(price: number, currency?: string): string;Line 1 is a comment. Line 2 says that price-format exports a function called formatPrice, which takes a number and an optional currency, and gives back a string. The word declare means "this exists, but its code is somewhere else", so the line ends with ; and has no body. Now cart.ts is checked:
import { formatPrice } from "./price-format"; const label = formatPrice(3);// ^?console.log(label);The import on line 1 has no error, and the query on line 4 says const label: string. When you import ./price-format, TypeScript looks for price-format.ts first, then for price-format.d.ts, and finds the types there. The browser still runs price-format.js and prints $3.00.
cart.ts imports one library made of two files. TypeScript reads price-format.d.ts to check it, and the browser runs price-format.js. Neither file holds both.A declaration file is something TypeScript believes. It never reads the JavaScript to check that the .d.ts is right. Pick what price-format.d.ts says, and watch what TypeScript makes of cart.ts:
There's no price-format.d.ts yet: only price-format.js.
import { formatPrice } from "./price-format"; const label = formatPrice(3);// ^?console.log(label);any1 error: line 1prints $3.00Without a .d.ts, TypeScript has no types for price-format.js. Line 1 gets Could not find a declaration file for module './price-format', and label is any: TypeScript allows anything, and checks nothing.
price: string makes TypeScript mark correct code, because the declaration is wrong: Argument of type 'number' is not assignable to parameter of type 'string'. returns number is worse. TypeScript reports nothing and calls label a number, while the code really makes text.
In a restaurant, the kitchen cooks and the menu describes. You order from the menu, and the waiter checks your order against it: no steak at a pizza place. Nobody walks into the kitchen to check. If the menu is out of date, you only find out when the plate arrives.
- In real life: The kitchen cooks the food
- In TypeScript: price-format.js does the work when the code runs
- In real life: The menu says what you can order and what comes out
- In TypeScript: price-format.d.ts says what you can call and what it gives back
- In real life: The waiter takes your order from the menu, without going into the kitchen
- In TypeScript: TypeScript checks cart.ts against the .d.ts, without reading the .js
- In real life: A menu that is out of date is found out only when the plate arrives
- In TypeScript: A wrong .d.ts is found out only when the code runs
A .d.ts file describes the types of code that lives somewhere else, usually JavaScript. It holds no code that runs. TypeScript reads it to check the code that uses the library, and believes it without looking at the JavaScript.
What a .d.ts file holds
A declaration file holds declarations: each one says that something exists and what its type is, but not how it works. Here is price-format.d.ts with two more:
export declare function formatPrice(price: number, currency?: string): string; export declare const defaultCurrency: string; export interface PriceOptions { currency: string; decimals: number;}- Line 1 declares a function: its parameters and return type, then
;where the body would be. - Line 3 declares a constant,
defaultCurrency, and says it holds text. It doesn't say which text: the value is in the JavaScript. - Lines 5 to 8 are an interface, written exactly as in a .ts file, because an interface is only a type anyway.
In a .d.ts, declare after export is optional, and you'll see real files written both ways. Code that runs is never allowed. Here is the same file with two lines of code in it, on purpose:
export declare const defaultCurrency: string = "$"; export function formatPrice(price: number, currency = "$"): string { return currency + price.toFixed(2);}Line 1 gives the constant a value, and TypeScript says Initializers are not allowed in ambient contexts. An ambient context is TypeScript's name for a place that only describes things, like a .d.ts file or anything after declare; an initializer is the = "$" part. Line 3 gives the function a body: An implementation cannot be declared in ambient contexts, which means "no working code in a description".
You've been using declaration files since your first TypeScript line. TypeScript comes with files like lib.es5.d.ts, which declares Math, and lib.dom.d.ts, which declares document and every other browser feature:
const biggest = Math.max(3, 4);// ^?const button = document.querySelector("button");// ^?console.log(biggest, button);The query on line 2 points at max and shows (method) Math.max(...values: number[]): number: any number of numbers in, a number out. The query on line 4 shows that querySelector("button") gives an HTMLButtonElement or null. Neither answer comes from running anything. Both come from declarations, and line 5 prints 4 null here, because this page has no button.
The table below compares the two kinds of TypeScript file. Read each row as a question, then the answer for each.
| Question | A .ts file | A .d.ts file |
|---|---|---|
| Does it hold code that runs? | Yes: functions with bodies, values, and their types. | No. Only types and declarations of things that exist somewhere else. |
| What does tsc make from it? | A .js file, and a .d.ts file too if declaration is on. | Nothing. It's only read for checking. |
| Can a function have a body? | Yes. | No. It ends with ; after its return type. |
| Who is it for? | The browser runs its JavaScript, and TypeScript checks it. | TypeScript, when it checks code that uses the library it describes. |
Types for packages: @types and DefinitelyTyped
Packages from npm (the npm lesson covers installing them) get their types in one of three ways. Some ship their own .d.ts files and name them in the types field of their package.json. Some have none, and people outside the project wrote them instead. And some have no types anywhere. Say your shop celebrates an order with canvas-confetti, a popular package written in plain JavaScript:
import confetti from "canvas-confetti"; confetti({ particleCount: 100, spread: 70 });Line 1 gets the same error as before, on purpose, with a second line of advice. The list under the code shows the whole message: Try `npm i --save-dev @types/canvas-confetti` if it exists, or add a .d.ts file of your own. Line 3 fires 100 pieces of confetti spread over 70 degrees; press Edit & run, then Show page, to see them.
DefinitelyTyped is a large project on GitHub where people write declaration files for JavaScript packages. Each one is published on npm as @types/ plus the package's name. You install it as a dev dependency, because only TypeScript needs it, never the browser:
npm install canvas-confetti
npm install --save-dev @types/canvas-confettiLine 1 installs the package itself. Line 2 installs its types into node_modules/@types/canvas-confetti, where TypeScript looks for them by itself, with nothing to set up. Here is a shortened version of what that package holds, written for this lesson: the real file declares the same function, with more options and comments.
export interface Options { particleCount?: number; spread?: number; colors?: string[];} export default function confetti(options?: Options): Promise<undefined> | null;Lines 1 to 5 describe the options object, with every option optional. Line 7 declares the package's default export: a function that takes those options and gives back a promise, or null in old browsers. With it installed, main.ts is checked:
import confetti from "canvas-confetti"; const done = confetti({ particleCount: 100, spread: 70 });// ^?No errors, and the query on line 4 shows const done: Promise<undefined> | null. Press Edit & run and change 100 to "lots": TypeScript marks it, because the types say particleCount is a number.
canvas-confetti; TypeScript reads @types/canvas-confetti.A package with a scope in its name, like @babel/core, gets types named with two underscores instead of the slash: @types/babel__core. The table below sums up where a package's types can come from. Read each row as: where the types are, what you install, and an example.
| Where the types are | What you install | An example |
|---|---|---|
In the package itself, named by types in its package.json | Just the package. | axios, dayjs |
On DefinitelyTyped, published as @types/ plus the package's name | The package, and its @types package as a dev dependency. | canvas-confetti, with @types/canvas-confetti |
| Nowhere | The package, then a .d.ts you write yourself. | A small or old package, or your company's own |
Writing a declaration for a package
When nobody has written types for a package, you write them yourself. For a file in your own project, you put a .d.ts beside it, as in the overview. For a package, you can't put files inside node_modules, because the next install would remove them. Instead you write declare module with the package's name, in a .d.ts anywhere in your project, often a types folder. Say your shop uses shop-analytics, a small package (made up for this lesson) that sends events to the shop's server and has no types:
declare module "shop-analytics" { export function track(event: string, data?: Record<string, string | number>): void;}Line 1 says: everything up to the closing brace on line 3 describes the package called "shop-analytics". Line 2 says it exports track, which takes an event name and optional data, and gives back nothing, which TypeScript writes as void. Record<string, string | number> means an object whose values are text or numbers. You only need to declare the parts of a package you use. Try three versions of this file:
There's no types/shop-analytics.d.ts yet.
import { track } from "shop-analytics"; track("add-to-cart", { product: "Tea", price: 3 });track(42);any1 error: line 1not checkedshop-analytics has no types, so line 1 gets Could not find a declaration file for module 'shop-analytics'. track is any, so line 4's mistake goes unnoticed.
The short form, declare module "shop-analytics";, is a quick way to make the error go away while you write real types: it says the package exists, and makes everything in it any. With the full declaration, line 4 gets Argument of type 'number' is not assignable to parameter of type 'string', because track needs an event name.
A declaration you write is a promise TypeScript can't check. Copy the types from the package's documentation, and try each declared function once to see what it really returns. When you update the package, look at your declaration again.
Letting tsc write declarations
When you write a library in TypeScript, you never write its .d.ts by hand. One compiler option makes tsc write it for you, beside each .js file:
{
"compilerOptions": {
"declaration": true,
"outDir": "dist"
}
}Line 3, "declaration": true, turns it on (the Setting up a project lesson covers tsconfig.json). Line 4 puts what tsc writes in a dist folder. Here is a library file:
export type Currency = "$" | "€" | "£"; const cache = new Map<number, string>(); export function formatPrice(price: number, currency: Currency = "$") { if (!cache.has(price)) cache.set(price, price.toFixed(2)); return currency + cache.get(price);}Line 1 exports a type with three currency signs. Line 3 makes a cache that only this file uses. Lines 5 to 8 export formatPrice, which keeps each formatted price in the cache. With declaration on, tsc writes this beside price-format.js:
export type Currency = "$" | "€" | "£";export declare function formatPrice(price: number, currency?: Currency): string;- Line 1 is the
Currencytype, copied as it is. - Line 2 is
formatPricewithout its body. The default= "$"became?, which means "you can leave it out", and the return type tsc worked out,string, is now written down. - The cache isn't there at all. It isn't exported, so code that uses the library never sees it.
declaration: true, tsc turns one .ts file into two: the JavaScript to run, and a .d.ts with its types for other projects to check against.tsc can do the same for a library written in JavaScript with JSDoc comments, which describe types in comments (the Types for JavaScript lesson covers JSDoc). Here is price-format.js with its types in a comment:
/**
* @param {number} price
* @param {string} [currency]
* @returns {string}
*/
export function formatPrice(price, currency = "$") {
return currency + price.toFixed(2);
}Lines 2 to 4 say that price is a number, currency is an optional string, and the function returns a string. This command makes a .d.ts from it, and no .js:
npx tsc price-format.js --allowJs --declaration --emitDeclarationOnly --outDir types--allowJs lets tsc read JavaScript, --declaration asks for .d.ts files, --emitDeclarationOnly skips writing JavaScript, and --outDir types puts the result in a types folder. It writes formatPrice(price: number, currency?: string): string. Last, a library tells TypeScript where its declarations are, in its package.json:
{
"name": "price-format",
"main": "./dist/price-format.js",
"types": "./dist/price-format.d.ts"
}Line 3, main, is the JavaScript that runs. Line 4, types, is the declaration file TypeScript reads. That's the first row of the table above: a package that ships its own types.
Types for an old coupons file
A shop's checkout still uses an old JavaScript file that applies coupon codes. Rewriting it in TypeScript can wait; describing it takes one line. Here is the file:
export function applyCoupon(total, code) {
if (code === "SAVE10") return (total * 0.9).toFixed(2);
return total.toFixed(2);
}Line 2 takes 10% off when the code is SAVE10, and line 3 keeps the total as it is otherwise. Both lines use toFixed(2), which gives back text with two decimals. So the declaration says the function returns a string:
export declare function applyCoupon(total: number, code: string): string;import { applyCoupon } from "./coupons"; const total = applyCoupon(10, "SAVE10");// ^?console.log("You pay $" + total);Line 3 applies SAVE10 to a total of 10. The query on line 4 shows const total: string, which matches what coupons.js really gives back, and line 5 prints You pay $9.00. Now sort some lines by whether they can go in a .d.ts file:
export declare function cartTotal(prices: number[]): number;export function cartTotal(prices: number[]) { return 0; }export interface Product { name: string; price: number }export type Price = number;export declare const shopName: string;export const shopName: string = "Corner Shop";console.log("Shop types loaded");export declare class Cart { add(name: string): void; }
Each line is checked on its own, as a file called shop.d.ts. Sort it by what TypeScript says.
Common mix-ups
- "A .d.ts file makes the JavaScript safe." No. It only tells TypeScript the types. The JavaScript runs exactly as before, and a wrong .d.ts lets mistakes through.
- "TypeScript checks the .d.ts against the JavaScript." No. It never reads the JavaScript. With returns number, it called a text result a number.
- "@types packages are part of the browser's download." No. They hold only types, so the browser never gets them. That's why they are dev dependencies.
- "declare module "x"; adds types for the package." No. The short form only quiets the error. Everything from the package is
any, so nothing is checked. - "I have to write a .d.ts for my own TypeScript library." No. Turn on
declaration, and tsc writes it from your code. - "Every package needs an @types package." No. Many, like axios, ship their own types. Install
@typesonly when TypeScript says it can't find a declaration file.
Practice exercises
Several exercises use coupons.js from the practical section, with a coupons.d.ts beside it. Press Edit & run on the starter code: TypeScript checks it with those files as you type. Try a guess first, then use the hints.
This is shop.d.ts. Which line will TypeScript mark?
export declare const shopName: string;
export declare function cartTotal(prices: number[]): number;
export function formatTotal(total: number): string {
return "$" + total.toFixed(2);
}export declare const shopName: string; export declare function cartTotal(prices: number[]): number; export declare function formatTotal(total: number): string;Line 5 gives formatTotal a body, and a .d.ts can't hold code: An implementation cannot be declared in ambient contexts. The fix keeps the types and drops the body: export declare function formatTotal(total: number): string;.
What type does TypeScript give total? Guess, then point at it, or press Edit & run and add a // ^? line under it.
import { applyCoupon } from "./coupons"; const total = applyCoupon(20, "NONE");console.log(total);import { applyCoupon } from "./coupons"; const total = applyCoupon(20, "NONE");// ^?console.log(total);coupons.d.ts says applyCoupon returns a string, so total is a string. The query on line 4 shows const total: string. It prints 20.00, text with two decimals.
Someone pasted the code into coupons.d.ts. Turn it into a declaration with no errors. Which word goes between export and function?
export function applyCoupon(total: number, code: string): string { if (code === "SAVE10") return (total * 0.9).toFixed(2); return total.toFixed(2);}export declare function applyCoupon(total: number, code: string): string;A declaration keeps the name, parameter types, and return type, and replaces the body with ;. declare says the code is somewhere else: in coupons.js.
Here the coupons.d.ts beside coupons.js says export declare function applyCoupon(total: number, code: number): string;, and line 3 gets an error. Which file needs fixing?
import { applyCoupon } from "./coupons"; console.log(applyCoupon(10, "SAVE10"));export declare function applyCoupon(total: number, code: string): string;checkout.ts is right: coupon codes are text. coupons.d.ts is wrong, because it says code: number. Fix the declaration to code: string, and line 3 has no error. It prints 9.00.
This time coupons.d.ts says export declare function applyCoupon(total: number, code: string): number;. TypeScript is happy. What does the code print?
import { applyCoupon } from "./coupons"; const total = applyCoupon(10, "SAVE10");console.log(total + 1);import { applyCoupon } from "./coupons"; const total = applyCoupon(10, "SAVE10");console.log(Number(total) + 1);The starter prints 9.001: the .d.ts says a number, but coupons.js returns the text "9.00", so + 1 joins. The fix makes the .d.ts say string, the truth, and turns the text into a number before adding: Number(total) + 1 is 10.
With declaration: true, tsc writes cart-tools.d.ts for this file. What line does it write for withTax?
const TAX_RATE = 0.2; export function withTax(price: number) { return price * (1 + TAX_RATE);}export declare function withTax(price: number): number;tsc drops the body and the private TAX_RATE, keeps price: number, and writes the return type it worked out: a number times a number is a number.
Check your understanding
For each question, keep two things apart: what the .d.ts tells TypeScript, and what the JavaScript really does when it runs. The quiz shows its code without the checker, so the error list doesn't give the answers away.
Question 1 of 7What does a .d.ts file hold?
Choose an answer to see the explanation.
Question 2 of 7Which line does TypeScript mark in this cart.d.ts?
Read the code, then predictTypeScriptexport declare function cartTotal(prices: number[]): number; export function itemCount(items: string[]): number { return items.length; }Choose an answer to see the explanation.
Question 3 of 7TypeScript says it could not find a declaration file for canvas-confetti. What's the usual first fix?
Choose an answer to see the explanation.
Question 4 of 7A .d.ts in the project says only
declare module "shop-analytics";. What does TypeScript say about line 3?Read the code, then predictTypeScriptimport { track } from "shop-analytics"; track(42);Choose an answer to see the explanation.
Question 5 of 7coupons.d.ts says applyCoupon returns a number, but coupons.js returns text. What does this print?
Read the code, then predict// coupons.d.ts says: applyCoupon(total: number, code: string): number import { applyCoupon } from "./coupons"; console.log(applyCoupon(10, "SAVE10") + 1);Choose an answer to see the explanation.
Question 6 of 7Which tsconfig option makes tsc write a .d.ts file for each .ts file?
Choose an answer to see the explanation.
Question 7 of 7Where does TypeScript find the types you install with @types/canvas-confetti?
Choose an answer to see the explanation.
Key takeaways
- A
.d.tsfile holds only types and declarations, for code that lives somewhere else. Nothing in it runs. declaremeans "this exists, but its code is somewhere else", so a declared function has no body.- TypeScript comes with declaration files for JavaScript and the browser, like lib.dom.d.ts.
- A package either ships its own types, has an
@typespackage from DefinitelyTyped, or needs a declaration you write withdeclare module. - TypeScript believes a .d.ts without reading the JavaScript, so keep hand-written declarations honest.
- For your own TypeScript library,
declaration: truemakes tsc write the .d.ts files.
Remember the one-liner.
A .d.ts is the menu, the JavaScript is the kitchen: TypeScript checks your order against the menu and trusts it.
Coming next: Globals & module augmentation. It uses declare for global variables, adds fields to Window, and extends another package's types.