cf.completefrontend
Open lab My progress
THE TYPESCRIPT FIELD GUIDE

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.

By the end, you can
  • 01
    Read a declaration fileSay what a .d.ts file holds, why it has no code that runs, and how TypeScript uses it to check yours.
  • 02
    Get types for a packageFix Could not find a declaration file with an @types package from DefinitelyTyped, or with a declaration of your own.
  • 03
    Write and generate declarationsDescribe a JavaScript library with declare, and have tsc write .d.ts files with declaration: 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:

price-format.js: a library with no typesJavaScript
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:

cart.ts imports the untyped library (an error on purpose)Pop out in the code editor (opens in a new tab)TypeScript
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:

price-format.d.ts: the types for price-format.jsTypeScript
// 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:

cart.ts with the declaration beside the libraryPop out in the code editor (opens in a new tab)TypeScript
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.

A card cart.ts at the top, with arrows to two windows. Left, an editor holds price-format.d.ts, types only, which TypeScript reads. Right, a browser holds price-format.js, code only, which the browser runs.cart.tseditorprice-format.d.tstypes onlyTypeScript reads thisbrowserprice-format.jscode onlythe browser runs this
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:

Playground: what price-format.d.ts says

There's no price-format.d.ts yet: only price-format.js.

cart.ts, checked with itPop out in the code editor (opens in a new tab)TypeScript
import { formatPrice } from "./price-format"; const label = formatPrice(3);//    ^?console.log(label);
TypeScript says label isany
What happens
TypeScript1 error: line 1
When it runsprints $3.00
Try it yourself

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

Each button changes only price-format.d.ts. price-format.js never changes. The real TypeScript compiler checks cart.ts with both files. Reset goes back to No .d.ts.

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.

Left, price-format.d.ts says formatPrice returns a number, and TypeScript believes it. Right, price-format.js really gives back the text $3.00. Between them a dashed line says the two are never compared.price-format.d.ts saysreturns: numberTypeScript believes thisnever comparedprice-format.js gives"$3.00", textthe browser runs this
With returns number, the .d.ts says one thing and the JavaScript does another. TypeScript only reads the .d.ts, so the mistake reaches the browser unnoticed.
Real-life analogyA restaurant menu and its kitchen

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
The main idea

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:

price-format.d.ts: a function, a constant, and an interfaceTypeScript
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:

Code in a declaration file (an error on purpose)TypeScript
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:

Types that come from TypeScript's own declaration filesPop out in the code editor (opens in a new tab)TypeScript
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.

A .ts file and a .d.ts file
QuestionA .ts fileA .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:

main.ts imports canvas-confetti (an error on purpose)Pop out in the code editor (opens in a new tab)TypeScript
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:

Install the package and its typesShell
npm install canvas-confetti
npm install --save-dev @types/canvas-confetti

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

node_modules/@types/canvas-confetti/index.d.ts, shortenedTypeScript
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:

main.ts with @types/canvas-confetti installedPop out in the code editor (opens in a new tab)TypeScript
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.

A crate node_modules holds two crates. canvas-confetti holds src/confetti.js, which the browser runs. @types/canvas-confetti holds index.d.ts, which TypeScript reads.src/confetti.jscanvas-confettiindex.d.ts@types/canvas-confettinode_modulesthe browser runs thisTypeScript reads this
After both installs, node_modules holds the package and its types side by side. The browser runs the code in 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 a package's types come from
Where the types areWhat you installAn example
In the package itself, named by types in its package.jsonJust the package.axios, dayjs
On DefinitelyTyped, published as @types/ plus the package's nameThe package, and its @types package as a dev dependency.canvas-confetti, with @types/canvas-confetti
NowhereThe 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:

types/shop-analytics.d.ts: types for a packageTypeScript
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:

Playground: declare the shop-analytics package

There's no types/shop-analytics.d.ts yet.

main.ts, checked with itTypeScript
import { track } from "shop-analytics"; track("add-to-cart", { product: "Tea", price: 3 });track(42);
TypeScript says track isany
What happens
TypeScript1 error: line 1
Line 4, track(42)not checked
Try it yourself

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

Each button changes only types/shop-analytics.d.ts. The real TypeScript compiler checks main.ts with it and with the installed package. Reset goes back to No declaration.

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.

Keep your declaration honest

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:

tsconfig.json for a libraryJSON
{
  "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:

price-format.ts: a library written in TypeScriptTypeScript
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:

price-format.d.ts, as tsc writes itTypeScript
export type Currency = "$" | "€" | "£";export declare function formatPrice(price: number, currency?: Currency): string;
  • Line 1 is the Currency type, copied as it is.
  • Line 2 is formatPrice without 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.
An editor holds price-format.ts with the type Currency and the function formatPrice. An arrow labelled tsc leads to two cards: price-format.js, the code without types, and price-format.d.ts, the types without code.editorprice-format.tstype CurrencyformatPrice() {…}you write thistscprice-format.jsthe code, without typesprice-format.d.tsthe types, without code
With 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:

price-format.js with JSDoc typesJavaScript
/**
 * @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:

Make a .d.ts from a JavaScript fileShell
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:

package.json points at the declarationsJSON
{
  "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:

coupons.js, from before the shop used TypeScriptJavaScript
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:

coupons.d.ts, written by handTypeScript
export declare function applyCoupon(total: number, code: string): string;
checkout.ts uses the old file with typesPop out in the code editor (opens in a new tab)TypeScript
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:

Fine in a .d.ts, or an error?
  • 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; }
Try it yourself
0 of 8 correct

Each line is checked on its own, as a file called shop.d.ts. Sort it by what TypeScript says.

Choose a category for every card. You can change an answer at any time; Reset clears them all.

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 @types only 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.

Exercise 1 · Warm-upFind the line TypeScript marks

This is shop.d.ts. Which line will TypeScript mark?

Starter codePop out in the code editor (opens in a new tab)TypeScript
export declare const shopName: string;

export declare function cartTotal(prices: number[]): number;

export function formatTotal(total: number): string {
  return "$" + total.toFixed(2);
}

Answer, then press Check. Spacing and letter case don’t matter.

    Exercise 2 · Warm-upRead a type from a declaration

    What type does TypeScript give total? Guess, then point at it, or press Edit & run and add a // ^? line under it.

    Starter codePop out in the code editor (opens in a new tab)TypeScript
    import { applyCoupon } from "./coupons"; const total = applyCoupon(20, "NONE");console.log(total);

    Answer, then press Check. Spacing and letter case don’t matter.

      Exercise 3 · PracticeTurn code into a declaration

      Someone pasted the code into coupons.d.ts. Turn it into a declaration with no errors. Which word goes between export and function?

      Starter codePop out in the code editor (opens in a new tab)TypeScript
      export function applyCoupon(total: number, code: string): string {  if (code === "SAVE10") return (total * 0.9).toFixed(2);  return total.toFixed(2);}

      Answer, then press Check. Spacing and letter case don’t matter.

        Exercise 4 · PracticeFind the wrong file

        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?

        Starter codePop out in the code editor (opens in a new tab)TypeScript
        import { applyCoupon } from "./coupons"; console.log(applyCoupon(10, "SAVE10"));

        Answer, then press Check. Spacing and letter case don’t matter.

          Exercise 5 · ChallengeSpot what TypeScript believed

          This time coupons.d.ts says export declare function applyCoupon(total: number, code: string): number;. TypeScript is happy. What does the code print?

          Starter codePop out in the code editor (opens in a new tab)TypeScript
          import { applyCoupon } from "./coupons"; const total = applyCoupon(10, "SAVE10");console.log(total + 1);

          Answer, then press Check. Spacing and letter case don’t matter.

            Exercise 6 · ChallengePredict what tsc writes

            With declaration: true, tsc writes cart-tools.d.ts for this file. What line does it write for withTax?

            Starter codePop out in the code editor (opens in a new tab)TypeScript
            const TAX_RATE = 0.2; export function withTax(price: number) {  return price * (1 + TAX_RATE);}

            Answer, then press Check. Spacing and letter case don’t matter.

              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.

              The declaration files quiz · 7 questionsScore: first tries count
              1. Question 1 of 7What does a .d.ts file hold?

                Choose an answer to see the explanation.

              2. Question 2 of 7Which line does TypeScript mark in this cart.d.ts?

                Read the code, then predictTypeScript
                export declare function cartTotal(prices: number[]): number;
                
                export function itemCount(items: string[]): number {
                  return items.length;
                }

                Choose an answer to see the explanation.

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

              4. 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 predictTypeScript
                import { track } from "shop-analytics";
                
                track(42);

                Choose an answer to see the explanation.

              5. Question 5 of 7coupons.d.ts says applyCoupon returns a number, but coupons.js returns text. What does this print?

                Read the code, then predictPop out in the code editor (opens in a new tab)TypeScript
                // 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.

              6. Question 6 of 7Which tsconfig option makes tsc write a .d.ts file for each .ts file?

                Choose an answer to see the explanation.

              7. 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.ts file holds only types and declarations, for code that lives somewhere else. Nothing in it runs.
              • declare means "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 @types package from DefinitelyTyped, or needs a declaration you write with declare module.
              • TypeScript believes a .d.ts without reading the JavaScript, so keep hand-written declarations honest.
              • For your own TypeScript library, declaration: true makes 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.

              CompleteFrontend Clear concepts. Working examples.