cf.completefrontend
Open lab My progress
THE TYPESCRIPT FIELD GUIDE

target, lib & module

Learn what target, lib, module, and moduleResolution change in tsconfig.json, why bundlers need isolatedModules, and which settings fit a Vite or Node project.

By the end, you can
  • 01
    Pick a target and a libSay which JavaScript tsc writes for a target, and why lib describes built-ins without adding them to a browser.
  • 02
    Set up importsChoose module and moduleResolution for a bundler or for Node, and fix an import path TypeScript rejects.
  • 03
    Keep bundlers safeExplain why tools that compile one file at a time need isolatedModules and export type.

Three settings that describe where your code runs

A shop page shows the newest item in the cart. The code is plain JavaScript that every current browser runs. Yet in this project, TypeScript marks it, on purpose here:

The newest cart item (an error on purpose)Pop out in the code editor (opens in a new tab)TypeScript
const cart = ["tea", "cake", "jam"];const newest = cart.at(-1);console.log(`Newest item: ${newest}`);

Line 1 makes a cart of three items. Line 2 reads the last one with .at(-1), a method that counts from the end of an array. Line 3 prints Newest item: jam when it runs. TypeScript marks line 2:

  • Property 'at' does not exist on type 'string[]' means "as far as I know, arrays have no at method".
  • Try changing the 'lib' compiler option to 'es2022' or later names the fix. The code is fine; the project's settings say .at() doesn't exist.

This example is checked with the setting "lib": ["es2021", "dom"], which says "the code runs where JavaScript stops at the 2021 edition". .at() came in 2022. That's what this lesson is about: the settings in tsconfig.json that describe where your code will run. Here are the ones that matter, in a typical browser project:

The output settings of a shop's tsconfig.jsonJSON
{  "compilerOptions": {    "target": "es2022",    "lib": ["es2022", "dom", "dom.iterable"],    "module": "esnext",    "moduleResolution": "bundler",    "strict": true  },  "include": ["src"]}
  • Line 3, target, picks the edition of JavaScript tsc writes. Older editions mean tsc rewrites more of your code.
  • Line 4, lib, lists the built-in things TypeScript knows about: methods like .at(), and the browser's document.
  • Lines 5 and 6, module and moduleResolution, say how import and export are written, and how TypeScript finds the file an import names.
A tsconfig.json window with three lines: "target": "es2022", "lib": ["es2022", "dom", …], and "module": "esnext". An arrow from each line points to a card: which JavaScript tsc writes, which built-ins TypeScript knows, and how imports work.tsconfig.json"target": "es2022""lib": ["es2022", "dom", …]"module": "esnext"Which JavaScript tsc writesWhich built-ins TypeScript knowsHow imports workthree questions, three settings
Three lines of tsconfig.json, three questions about where your code runs. target picks the JavaScript tsc writes, lib lists the built-in things TypeScript believes exist, and module decides how imports are written and found.
Real-life analogyCooking in a friend's kitchen

You're cooking dinner at a friend's place. Before you start, you ask about their kitchen. Their oven can't grill, so you change the recipe to bake instead. You read their pantry list so you only plan dishes with what they have. And you remember that a name on the list is only a note: if the jar is empty, the dish still fails.

In real life: You ask what kind of kitchen your friend has
In TypeScript: tsconfig.json says where your code will run
In real life: Their old oven has no grill, so you rewrite the step to use the oven
In TypeScript: target makes tsc rewrite syntax an older browser doesn't know
In real life: You check their pantry list before you plan a dish
In TypeScript: lib tells TypeScript which built-in methods exist
In real life: Writing "saffron" on the pantry list doesn't put saffron on the shelf
In TypeScript: Adding to lib doesn't add a method to an old browser
The main idea

Output settings describe where your code runs. target picks which JavaScript tsc writes. lib lists the built-in things TypeScript believes exist. module and moduleResolution say how imports are written and found. None of them adds a missing feature to a browser.

target: which JavaScript tsc writes

JavaScript gets a new edition every year: ES2015, ES2016, and so on (ES is short for ECMAScript, the language's official name). Each edition adds syntax, like ?. in ES2020. target names the oldest edition your users' browsers understand. When your code uses newer syntax, tsc rewrites it into older syntax that does the same thing. This is called downleveling (the How new syntax is downleveled lesson shows how). Here is a small class that picks a page theme:

theme-picker.ts, a class with a field, ?. and ??Pop out in the code editor (opens in a new tab)TypeScript
type Settings = { theme?: { color: string } }; class ThemePicker {  fallback = "light";  pick(settings: Settings) {    return settings.theme?.color ?? this.fallback;  }} const picker = new ThemePicker();console.log(`Theme: ${picker.pick({})}`);
  • Line 1 says Settings may have a theme with a color.
  • Line 4 is a class field: fallback is set on every new ThemePicker, written inside the class body (Class basics covers fields).
  • Line 6 reads the color with ?., which stops at a missing theme, and falls back with ?? (both are in Nullish coalescing & optional chaining).
  • Line 11 picks a theme for empty settings and prints Theme: light.

Now pick a target and read what tsc writes for each one:

Playground: one class, four targets
theme-picker.ts, what you writePop out in the code editor (opens in a new tab)TypeScript
type Settings = { theme?: { color: string } }; class ThemePicker {  fallback = "light";  pick(settings: Settings) {    return settings.theme?.color ?? this.fallback;  }} const picker = new ThemePicker();console.log(`Theme: ${picker.pick({})}`);
"target""es5"
theme-picker.js, what tsc writes for es5Pop out in the code editor (opens in a new tab)JavaScript
var ThemePicker = /** @class */ (function () {
    function ThemePicker() {
        this.fallback = "light";
    }
    ThemePicker.prototype.pick = function (settings) {
        var _a, _b;
        return (_b = (_a = settings.theme) === null || _a === void 0 ? void 0 : _a.color) !== null && _b !== void 0 ? _b : this.fallback;
    };
    return ThemePicker;
}());
var picker = new ThemePicker();
console.log("Theme: ".concat(picker.pick({})));
Rewrittenthe class, the field, ?., ??, const, and the template literal
PrintsTheme: light
Try it yourself

ES5 is from 2009, before classes, const, and template literals. tsc rewrites all of them: the class becomes a function with ThemePicker.prototype.pick, const becomes var, and the template literal becomes .concat(). ?. and ?? become a long check against null and undefined. It still prints Theme: light.

TypeScript 5.9 wrote each output with ts.transpileModule in this lesson's tests, and the page shows the saved result. Edit & run on the output runs it in your browser. Reset goes back to es5.

Start at es5 and go up. The output shrinks as the target gets newer, because there's less to rewrite. At es2022 it's your code without its types. Every version prints Theme: light: downleveling changes how the code is written, not what it does. The table below lists when the syntax you use most arrived. Read each row as: the edition, what it added, and what tsc does when your target is older.

When common syntax arrived, and what tsc does below it
EditionWhat it addedWith an older target
ES2015const, let, classes, arrow functions, and template literals.Below ES2015, const becomes var, a class becomes a function, and a template literal becomes .concat().
ES2017async functions and await.Below ES2017, an async function becomes a helper called __awaiter that runs a generator.
ES2020?. (optional chaining) and ?? (nullish coalescing).Below ES2020, both become a long check against null and undefined.
ES2022Class fields, written inside the class body, and #private fields.Below ES2022, a field moves into the constructor, and a #private field becomes a WeakMap.

Here is the catch. target rewrites syntax: the symbols and keywords of the language. It never adds methods. Here is what tsc writes for the overview's code with "target": "es2017":

What tsc writes for the newest-item code with target es2017Pop out in the code editor (opens in a new tab)JavaScript
const cart = ["tea", "cake", "jam"];
const newest = cart.at(-1);
console.log(`Newest item: ${newest}`);

It's the same three lines. cart.at(-1) on line 2 is a method call, which is ordinary syntax, so nothing needs rewriting. In a browser without .at(), like one from 2020, line 2 stops the code:

What a browser without .at() showsText
TypeError: cart.at is not a function

A missing method needs a polyfill, a piece of code that adds it while the page runs (the Transpilers & polyfills lesson covers both). TypeScript never adds polyfills.

Two more facts about target. If you leave it out, TypeScript 5.9 uses es5, which rewrites almost everything; TypeScript 6.0 uses the newest edition instead and deprecates es5, so write the target you want. And if a bundler like Vite builds your page, it ignores this setting and uses its own: build.target in Vite's settings. In such a project, target still matters for checking, as the next section shows.

lib: which built-in types TypeScript knows

TypeScript doesn't look inside a browser to see which methods exist. It reads lib files: files that describe, in types, everything JavaScript and the browser have built in. Each edition of JavaScript has one, and so does the browser's page. Here is the part of lib.es2022.array.d.ts that describes .at() (the comment is shortened):

How the ES2022 lib describes .at()TypeScript
interface Array<T> {  /**   * Returns the item located at the specified index.   */  at(index: number): T | undefined;}

Line 1 adds to the Array type. Line 5 says an array has a method at that takes a number and gives back an item or undefined. There's no code here that finds the item; it's only a description. The lib setting picks which of these files TypeScript reads:

  • "es2022" describes JavaScript up to the 2022 edition: Array, Math, Promise, .at(), and so on.
  • "dom" describes the browser's page: document, elements, events, fetch, and console.
  • "dom.iterable" adds looping with for...of over lists the page gives you, like document.querySelectorAll("li"). In TypeScript 6.0 it's part of "dom".

When you write lib, your list replaces the whole default. Try four lists on the same code. It reads the newest item, then writes it into every row of a list on the page:

Playground: change the lib list
Fill the rows with the newest itemPop out in the code editor (opens in a new tab)TypeScript
const cart = ["tea", "cake", "jam"];const newest = cart.at(-1); const rows = document.querySelectorAll("li");for (const row of rows) {  row.textContent = newest ?? "";}console.log(`Updated ${rows.length} rows`);
"lib"["es2022", "dom", "dom.iterable"]
What happens
TypeScriptNo errors
On the pageUpdated 3 rows
Try it yourself

TypeScript knows everything this code uses: .at() from ES2022, document and console from dom, and looping over a list of elements from dom.iterable. No errors. On the page, it fills all 3 rows with jam and prints Updated 3 rows.

The real TypeScript compiler checks the same code with the lib list you pick. Edit & run runs it on a page with three rows. Reset goes back to the first list.

With all three entries, there are no errors. Drop to es2021 and line 2 is marked, as in the overview. Leave out dom.iterable and the for...of loop on line 5 is marked. Leave out dom and TypeScript can't find document or even console. In every case, press Edit & run: the code still fills the rows and prints Updated 3 rows, because the browser has all of these things. lib only changes what TypeScript believes.

Left: a lib file holds the line at(index): T | undefined, so TypeScript reports no error for cart.at(-1). Right: a browser from 2020 has no .at() method, so cart.at(-1) throws a TypeError.What TypeScript readslib fileat(index): T | undefineda description, not codecart.at(-1)no errorWhat a 2020 browser hasbrowserno .at() methodthe real methods onlycart.at(-1)TypeError
lib is a description. With es2022 in it, TypeScript believes .at() exists and reports nothing. A browser from 2020 never had the method, so cart.at(-1) still throws a TypeError there.

That cuts both ways. A lib entry the browser doesn't really have hides a crash, as the TypeError above showed. So pick lib to match the oldest browser you support, or add a polyfill for each newer method you use.

When you leave lib out, TypeScript picks it from target: "target": "es2022" gives you es2022, dom, and dom.iterable (and three smaller ones you won't need to think about). Code that runs in Node, not a browser, leaves out dom and adds Node's own types with "types": ["node"], after installing the @types/node package.

module and moduleResolution: how imports work

Real projects split code into files that import from each other (the Import & export lesson covers the syntax). The shop keeps its price formatting in one file:

export function formatPrice(price: number) {  return `$${price.toFixed(2)}`;}

Line 1 exports formatPrice, and line 2 turns a number like 7 into $7.00. Another file imports it. Pick a module setting to see how tsc writes that import:

Playground: one file, three module settings
import { formatPrice } from "./format"; export function cartLabel(total: number) {  return `Total: ${formatPrice(total)}`;} console.log(cartLabel(7));
"module""esnext"
cart.js, what tsc writes for esnextJavaScript
import { formatPrice } from "./format";
export function cartLabel(total) {
    return `Total: ${formatPrice(total)}`;
}
console.log(cartLabel(7));
Try it yourself

esnext writes ES modules: the import and export lines stay as you wrote them, and only the types go. Browsers and bundlers read this form.

TypeScript 5.9 wrote each output with ts.transpileModule in this lesson's tests; the page shows the saved result. Reset goes back to esnext.

In cart.ts, line 1 imports formatPrice, line 3 exports cartLabel, and line 7 prints Total: $7.00. With esnext and preserve, the output keeps import and export: these are ES modules, the form browsers and bundlers read. With commonjs, it uses require and exports, the form Node used before ES modules (the CommonJS vs ES modules lesson compares them).

module also tells TypeScript what the place your code runs can do, so it changes checking even when tsc writes nothing. This file waits for prices with await outside any function, which only newer module forms allow. It's checked with "module": "es2015", and the error is on purpose:

Top-level await with module es2015 (an error on purpose)Pop out in the code editor (opens in a new tab)TypeScript
import { formatPrice } from "./format"; const prices = await Promise.resolve([3, 4]);console.log(formatPrice(prices[0] + prices[1]));

Line 3 waits for a list of two prices, and line 4 prints their total, $7.00. TypeScript marks line 3: Top-level 'await' expressions are only allowed when the 'module' option is set to 'es2022', 'esnext', and a few more. In plain words: await at the top of a file came with ES2022 modules, and es2015 modules don't have it. With this lesson's default, esnext, the error is gone.

The second setting, moduleResolution, is how TypeScript finds the file an import names. Bundlers and Node find files differently, so TypeScript has a mode for each:

  • "bundler" works like Vite: "./format" is enough, because the bundler tries endings like .ts and .js itself.
  • "nodenext" works like Node (the Module resolution lesson explains Node's rules). In an ES module, Node wants the whole file name. "module": "nodenext" sets it for you.

Try both. The project has cart.ts, format.ts, and a package.json with "type": "module", which tells Node the files are ES modules. Type a path on line 1:

Playground: type the import path
cart.tsTypeScript
import { formatPrice } from "./format"; console.log(formatPrice(3));
"moduleResolution""nodenext"
What happens
Line 1 imports./format
TypeScript1 error: line 1
Try it yourself

Node needs the whole file name in an ES module, so nodenext says Relative import paths need explicit file extensions and suggests the fix: Did you mean './format.js'?

The real TypeScript compiler checks cart.ts next to format.ts each time you type, with the setting you pick. The playground only checks; it doesn't run the code. Reset goes back to nodenext and ./format.

With nodenext, ./format gets Relative import paths need explicit file extensions in ECMAScript imports, and the message suggests ./format.js. That surprises everyone the first time: the file you wrote is format.ts. But tsc turns it into format.js, and that's the file Node loads. tsc never changes the paths you write, so you write the name the file will have. While checking, TypeScript knows that ./format.js means format.ts.

Left, bundler: the import from "./format" has an arrow to format.ts. Right, nodenext: the import from "./format.js" has one arrow to format.ts, labelled while checking, and one to dist/format.js, labelled when Node runs it."moduleResolution": "bundler"from "./format"format.tsfound"moduleResolution": "nodenext"from "./format.js"format.tsdist/format.jswhile checkingwhen Node runs it
With bundler, "./format" is enough: the bundler tries the endings. With nodenext, you write the name the file has after the build, "./format.js". TypeScript reads format.ts for the types; Node later loads format.js.

The table below matches the two settings to where your code runs. Read each row as: the situation, then the two settings.

module and moduleResolution for common projects
Where the code runsmodulemoduleResolution
A bundler like Vite builds the page"esnext" or "preserve""bundler": imports can leave out the ending, like ./format.
Node runs the JavaScript tsc writes"nodenext"Set by nodenext for you. ES module imports need the ending, like ./format.js.
Node runs your .ts files directly"nodenext"The same as above, with .ts endings. The next lesson covers this setup.

Bundlers and isolatedModules

tsc reads your whole project at once. Fast build tools don't. Vite, esbuild, SWC, and Babel turn each .ts file into JavaScript on its own, without looking at the files it imports (the Bundlers & build tools lesson covers what they do). That's what makes them fast. But some TypeScript code can only be compiled correctly by looking at another file. Here is a types.ts with a type and a function:

export type CartItem = { name: string; price: number }; export function formatPrice(price: number) {  return `$${price.toFixed(2)}`;}

Line 1 exports a type, CartItem. Line 3 exports a function, formatPrice. Many projects add an index.ts that passes on everything from a folder, so other files import from one place. Pick how index.ts passes them on:

Playground: pass on a type from types.ts
index.tsTypeScript
export { CartItem, formatPrice } from "./types";
"isolatedModules"false
index.js from tsc, which reads both filesJavaScript
export { formatPrice } from "./types";
index.js from a tool that sees one fileJavaScript
export { CartItem, formatPrice } from "./types";
TypeScriptNo errors
One-file output in a browserSyntaxError: no export named 'CartItem'
Try it yourself

tsc reads both files, sees that CartItem is only a type, and leaves it out of index.js. A tool that compiles one file at a time can't see types.ts, so it keeps CartItem, and the browser stops: types.js has no export called CartItem. TypeScript reports nothing.

The real compiler checks index.ts next to types.ts. Both outputs were written by TypeScript 5.9 in this lesson's tests: once for the whole project, once for index.ts alone, the way a bundler compiles it. Reset goes back to the first button.

With the first button, tsc and the one-file tool disagree. tsc read types.ts, knew CartItem is only a type, and left it out. The one-file tool kept it, and a browser stops with:

What the browser says about the one-file outputText
SyntaxError: The requested module './types.js' does not provide an export named 'CartItem'

types.js has no CartItem: types don't exist in JavaScript. The setting "isolatedModules": true fixes this before it happens. It tells TypeScript: "warn me about anything a tool that sees one file at a time would get wrong". It doesn't change what tsc writes. The second button shows the warning, and the last two show the fix: mark the type with type, so any tool can remove it from this file alone.

Left, tsc: index.ts and types.ts, with an arrow between them, so tsc knows CartItem is a type and leaves it out. Right, a one-file tool: index.ts alone, with the question: is CartItem a type or a value? It keeps it.tscindex.tstypes.tsreads both filesCartItem is a typeleaves it outA one-file toolindex.tssees one file onlyCartItem: type or value?keeps it
tsc reads every file, so it knows CartItem is only a type. A tool that compiles one file at a time sees index.ts alone. It can't tell, so it keeps CartItem, and the browser can't find it.

The table below lists the three cases TypeScript's documentation gives for isolatedModules. Read each row as: the code, the message, and what to write instead.

What isolatedModules warns about
When you writeTypeScript saysWrite instead
Passing on a type with plain export { CartItem }Re-exporting a type when 'isolatedModules' is enabled requires using 'export type'Write export type { CartItem }, or type in front of the name.
Reading a declare const enum from a library's typesCannot access ambient const enums when 'isolatedModules' is enabledUse a plain object or a union of strings instead of the const enum.
A namespace in a file with no import or exportNamespaces are not allowed in global script files when 'isolatedModules' is enabledAdd an import or export, or set moduleDetection to force.

Vite's own documentation says to set isolatedModules to true. Many projects go one step further with "verbatimModuleSyntax": true, which turns isolatedModules on and also asks you to mark every imported type with type. The Modules & type imports lesson covers it.

The output settings of a Vite checkout page

Here are the output settings Vite's TypeScript starter gives a new checkout page (the vanilla-ts template, shortened to the lines this lesson covers):

tsconfig.json from Vite's vanilla-ts template (shortened)JSON
{
  "compilerOptions": {
    "target": "es2023",
    "module": "esnext",
    "lib": ["ES2023", "DOM"],
    "types": ["vite/client"],
    "skipLibCheck": true,

    "moduleResolution": "bundler",
    "allowImportingTsExtensions": true,
    "verbatimModuleSyntax": true,
    "moduleDetection": "force",
    "noEmit": true
  },
  "include": ["src"]
}
  • Line 3, target: ES2023. Vite ignores it when it builds, but TypeScript still uses it for checking.
  • Line 4, module: esnext, ES modules, the form Vite reads.
  • Line 5, lib: ES2023 and the page. Line 6, types, adds Vite's own types, like import.meta.env. Line 7, skipLibCheck, skips checking the type files of installed packages, which saves time.
  • Line 9, moduleResolution: bundler, so imports can leave out the ending. Line 10 allows imports that end in .ts, because Vite reads those files itself.
  • Line 11, verbatimModuleSyntax, makes Vite's one-file compiling safe. Line 12, moduleDetection, treats every file as a module, even one with no import.
  • Line 13, noEmit: tsc only checks. Vite writes the JavaScript.

The template is made for TypeScript 6.0, where "DOM" includes DOM.Iterable. With TypeScript 5.9, its lib line has a gap. Here is checkout code with the template's lib, checked with 5.9, and the error is on purpose:

Rows of a checkout list, with the template's lib in TypeScript 5.9 (an error on purpose)Pop out in the code editor (opens in a new tab)TypeScript
const rows = document.querySelectorAll("li");for (const row of rows) {  row.textContent = row.textContent?.toUpperCase() ?? "";}console.log(`Updated ${rows.length} rows`);

Line 1 finds every row of the list. Lines 2 to 4 write each row's text in capitals, and line 5 prints Updated 3 rows. TypeScript marks the loop on line 2: Type 'NodeListOf<HTMLLIElement>' must have a '[Symbol.iterator]()' method. In plain words: "I don't know this list can be looped over". Add "DOM.Iterable" to lib, and the same code has no errors:

The same rows with DOM.Iterable in libPop out in the code editor (opens in a new tab)TypeScript
const rows = document.querySelectorAll("li");for (const row of rows) {  row.textContent = row.textContent?.toUpperCase() ?? "";}console.log(`Updated ${rows.length} rows`);

Every message in this lesson points at one setting. Sort the problems below by what fixes them.

Which setting fixes it?
  • An old browser stops at ?. with a SyntaxError
  • tsc's output has var where you wrote const
  • Property 'at' does not exist on type 'string[]'. Do you need to change your target library?
  • Cannot find name 'document'.
  • Relative import paths need explicit file extensions in ECMAScript imports
  • Top-level 'await' expressions are only allowed when the 'module' option is set to
  • Re-exporting a type when 'isolatedModules' is enabled requires using 'export type'.
  • The browser says a module does not provide an export named 'CartItem'
Try it yourself
0 of 8 correct

Sort each problem by the change that fixes it.

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

Common mix-ups

  • "An older target makes .at() work in old browsers." No. target rewrites syntax like ?.. Methods stay as you wrote them, and a browser without them throws a TypeError.
  • "Adding es2023 to lib makes .toSorted() work everywhere." No. lib is a description for TypeScript. It adds nothing to the browser.
  • ""lib": ["es2022"] adds ES2022 to what I had." No. Your list replaces the default. Without "dom" in it, document and console are gone.
  • "With noEmit, target and module don't matter." No. They still decide the default lib and checks like top-level await.
  • "tsc turns ./format into ./format.js for Node." No. tsc writes your import paths as they are. With nodenext, you write ./format.js yourself.
  • "isolatedModules changes the JavaScript tsc writes." No. It only adds warnings about code that tools compiling one file at a time would get wrong.

target and lib are the easiest pair to mix up. The table compares them. Read each row as a question, then each setting's answer.

target and lib, side by side
Questiontargetlib
What does it change?Which syntax tsc writes in the JavaScript.Which built-in things TypeScript believes exist while it checks.
Does it change the JavaScript tsc writes?Yes. Older targets mean more rewriting.No. It only changes which errors you see.
Does it add missing methods to old browsers?No. It rewrites syntax, never methods like .at().No. A polyfill or a newer browser has to provide them.
What if you leave it out?TypeScript 5.9 uses es5. TypeScript 6.0 uses the newest edition.TypeScript picks it from target and adds dom, plus dom.iterable from ES2015 on.

Practice exercises

Some exercises are TypeScript code, checked with the settings they name; others are a tsconfig.json to fix. For code, press Edit & run on the starter: the errors update as you type. Try a guess first, then use the hints.

Exercise 1 · Warm-upRead which lib a method needs

This project has "lib": ["es2022", "dom"]. TypeScript marks line 2. Which lib entry does toSorted need?

Starter codePop out in the code editor (opens in a new tab)TypeScript
const prices = [4, 3, 5];const cheapestFirst = prices.toSorted();console.log(cheapestFirst);

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

    Exercise 2 · Warm-upFind the oldest target that keeps ?. and ??

    Which is the oldest target for which tsc leaves line 4 exactly as written?

    Starter codePop out in the code editor (opens in a new tab)TypeScript
    type Settings = { theme?: { color: string } }; function themeColor(settings: Settings) {  return settings.theme?.color ?? "light";}console.log(themeColor({}));

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

      Exercise 3 · PracticeFix an import for nodenext

      This project uses "module": "nodenext", and its package.json says "type": "module". Fix line 1. What should the path be?

      Starter codePop out in the code editor (opens in a new tab)TypeScript
      import { formatPrice } from "./format"; console.log(formatPrice(4));

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

        Exercise 4 · PracticeMake index.ts safe for a bundler

        This project has "isolatedModules": true, and index.ts passes on what types.ts exports. Fix the warning. Which name needs type?

        Starter codePop out in the code editor (opens in a new tab)TypeScript
        export { CartItem, formatPrice } from "./types";

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

          Exercise 5 · ChallengeFind the crash TypeScript can't see

          This project has "target": "es2017" and "lib": ["es2023", "dom"]. TypeScript reports nothing, and a new browser prints 3. What does a 2021 browser show when it runs line 8? Then rewrite it to work there.

          Starter codePop out in the code editor (opens in a new tab)TypeScript
          type Order = { id: number; paid: boolean }; const orders: Order[] = [  { id: 1, paid: true },  { id: 2, paid: false },  { id: 3, paid: true },];const lastPaid = orders.findLast((order) => order.paid);console.log(lastPaid?.id);

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

            Exercise 6 · ChallengeSet up a script that Node runs

            A teammate copied the shop's tsconfig.json for a script in scripts/ that tsc builds and Node runs. Fix its output settings. What should module be?

            Starter codeJSON
            {
              "compilerOptions": {
                "target": "es2022",
                "lib": ["es2022", "dom"],
                "module": "esnext",
                "moduleResolution": "bundler",
                "outDir": "dist",
                "strict": true
              },
              "include": ["scripts"]
            }

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

              Check your understanding

              For each question, ask which setting is involved: target for syntax, lib for built-in methods, module and moduleResolution for imports. The quiz shows its code without the checker, so the error list doesn't give the answers away.

              The target, lib & module quiz · 7 questionsScore: first tries count
              1. Question 1 of 7What does target change?

                Choose an answer to see the explanation.

              2. Question 2 of 7What does TypeScript say about line 3 here?

                Read the code, then predictPop out in the code editor (opens in a new tab)TypeScript
                // tsconfig.json: "lib": ["es2021", "dom"]
                const cart = ["tea", "cake"];
                console.log(cart.at(-1));

                Choose an answer to see the explanation.

              3. Question 3 of 7With this tsconfig, a browser from 2020 runs the JavaScript tsc writes. What happens?

                Read the code, then predictPop out in the code editor (opens in a new tab)TypeScript
                // tsconfig.json: "target": "es2017", "lib": ["es2023", "dom"]
                const cart = ["tea", "cake"];
                console.log(cart.at(-1));

                Choose an answer to see the explanation.

              4. Question 4 of 7Which lib list knows document but, in TypeScript 5.9, can't loop over document.querySelectorAll("li") with for...of?

                Choose an answer to see the explanation.

              5. Question 5 of 7This file is an ES module checked with nodenext. What does TypeScript want on line 2?

                Read the code, then predictTypeScript
                // tsconfig.json: "module": "nodenext"; package.json: "type": "module"
                import { formatPrice } from "./format";

                Choose an answer to see the explanation.

              6. Question 6 of 7What does turning on isolatedModules change in the JavaScript tsc writes?

                Choose an answer to see the explanation.

              7. Question 7 of 7Why does a bundler need export type { CartItem } instead of export { CartItem }?

                Choose an answer to see the explanation.

              Key takeaways

              • target picks which JavaScript tsc writes. Older targets rewrite newer syntax; no target adds methods.
              • lib lists the built-ins TypeScript believes exist. Your list replaces the default, so keep dom for a page.
              • In TypeScript 5.9, looping over a NodeList needs dom.iterable; 6.0 puts it inside dom.
              • module decides how imports are written and which module features are allowed, like top-level await.
              • moduleResolution bundler lets you leave out endings; nodenext wants ./format.js.
              • Bundlers compile one file at a time. isolatedModules warns about code they'd get wrong; export type fixes it.

              Remember the one-liner.
              target rewrites syntax, lib describes built-ins, module shapes imports, and none of them changes what the browser has.

              Coming next: Type checking vs type stripping. Vite, esbuild, and Node run TypeScript without checking it, so you'll see where tsc still fits, from your editor to CI.

              CompleteFrontend Clear concepts. Working examples.