7. TypeScript

JavaScript reports a misspelled property or a missing argument only when the line runs, often in a user's browser. TypeScript adds a static type system: you describe the shape of your data, and the compiler flags mistakes as you type. The types are erased at build time, leaving the JavaScript of Chapter 6.This chapter follows the TypeScript Handbook from everyday types and narrowing to generics, conditional and mapped types, then covers classes, modules, declaration files and tsconfig.json. It targets TypeScript 7.0, the native compiler released in July 2026. Every example was checked with the real compiler, and error messages appear exactly as tsc prints them.What you will learn:
  • Why teams adopt TypeScript, how the compiler works and how to run .ts files in Node.js, Deno and Bun

  • Everyday types, narrowing, functions, object types and type manipulation

  • Classes, modules, declaration files and project configuration

  • Migration, Zod, typescript-eslint, and what changed from TypeScript 5.0 to 7.0


  • 7.1 Introduction

    TypeScript is a superset of JavaScript: JavaScript syntax is also TypeScript syntax, extended with annotations, interfaces and generics. Microsoft released version 0.8 in October 2012; its designer, Anders Hejlsberg, also created Turbo Pascal and C#. In August 2025 TypeScript overtook Python and JavaScript as the most used language on GitHub (Octoverse 2025). Runtime semantics (scopes, prototypes, the event loop) belong to Chapter 6; this section covers what types buy you, the 6.0 and 7.0 compilers, their internals and the tools that run .ts files.

    7.1.1 Why TypeScript?

    JavaScript is dynamically typed: a variable's type is known only at run time, and reading a property that does not exist quietly produces undefined (Section 6.8). Arithmetic on undefined then yields NaN, and the bug surfaces far from its cause. This shopping-cart function receives an item whose quantity property was named quantity instead of qty:

    function total(items) {

    return items.reduce((sum, item) => sum + item.price * item.qty, 0);

    }

    console.log(total([{ price: 9.5, quantity: 2 }]));

    Listing 7.1: A misnamed property that JavaScript runs silently (cart.js) NaN Output of Listing 7.1 Nothing fails; the wrong total just flows into the page. In TypeScript you describe the expected shape once with an interface, annotate the parameter, and let the compiler compare every call against it:

    interface Item {

    price: number;

    qty: number;

    }

    function total(items: Item[]): number {

    return items.reduce((sum, item) => sum + item.price * item.qty, 0);

    }

    console.log(total([{ price: 9.5, quantity: 2 }]));

    Listing 7.2: The same function with types (cart.ts)

    $ npx tsc --noEmit cart.ts

    cart.ts(10,34): error TS2353: Object literal may only specify known properties, and 'quantity'

    does not exist in type 'Item'.

    Output of Listing 7.2 The error names the file, line, column and a stable code (TS2353) you can search for. Rename the property to qty and node cart.ts prints 19. The same types power your editor's completion, go-to-definition and safe renames, and when you change a type the compiler lists every line to fix. Warning: Types are erased before the code runs, so TypeScript cannot check data that arrives at run time, such as a fetch() response or form input. Validate that data with a library such as Zod (Section 7.13). TypeScript pays off most in code that lives long, has many contributors or is shared as a library. A short script may need nothing more than JSDoc comments (Section 7.11).

    7.1.2 TypeScript 6.0 and the Native Go-based TypeScript 7.0 Compiler

    Through version 6.0 the compiler was written in TypeScript and ran on Node.js. In March 2025 Microsoft announced a port to Go, and two 2026 releases complete the move:
  • TypeScript 6.0 (March 23, 2026) is the last release built on the JavaScript codebase. It is a bridge: strict is on by default, target defaults to es2025, module to esnext, and types to [] (no automatic @types packages). Legacy options are deprecated; "ignoreDeprecations": "6.0" silences the errors for now.

  • TypeScript 7.0 (July 8, 2026) is the Go port, published as the same typescript npm package with the same tsc command. Its type checker is structurally identical to 6.0's, so it reports the same errors, but native code and shared-memory multithreading make full builds typically 8 to 12 times faster.

  • Codebase TypeScript 6.0 TypeScript 7.0 Speedup VS Code 125.7 s 10.6 s 11.9x Sentry 139.8 s 15.7 s 8.9x Playwright 12.8 s 1.47 s 8.7x Table 7.1: Full-build times from the TypeScript 7.0 announcement TypeScript 7.0 removes what 6.0 deprecated: target: "es5", downlevelIteration, moduleResolution: "node" (node10) and "classic", the amd, umd and systemjs module formats, baseUrl and esModuleInterop: false. A legacy tsconfig.json now stops the build:

    $ npx tsc --noEmit

    tsconfig.json(3,15): error TS5108: Option 'target=ES5' has been removed. Please remove it from your configuration.

    tsconfig.json(4,25): error TS5108: Option 'moduleResolution=node10' has been removed. Please remove it from your configuration.

    tsconfig.json(5,5): error TS5102: Option 'baseUrl' has been removed. Please remove it from your configuration.

    Use '"paths": {"*": ["./src/*"]}' instead.

    Listing 7.3: TypeScript 7.0 rejecting removed options New command-line flags control parallelism: --checkers N sets the number of type-checking workers (default 4), --builders N builds project references concurrently, and --singleThreaded turns parallelism off. TypeScript 6.0's --stableTypeOrdering orders types as 7.0 does, so both versions produce comparable output during migration. Warning: TypeScript 7.0 ships without a programmatic API; a new one is planned for 7.1. Tools that import the compiler (Vue, Svelte, Astro and MDX tooling) still need 6.0. Keep both with npm aliases in devDependencies: "typescript": "npm:@typescript/typescript6@^6.0.2" (6.0 API plus tsc6) and "@typescript/native": "npm:typescript@^7.0.2" (tsc). History: Previews shipped as @typescript/native-preview with a tsgo command. The team chose Go because idiomatic Go resembles the existing code closely enough for a file-by-file port.

    7.1.3 How the Compiler Works Under the Hood

    tsc is a pipeline of five phases, each consuming the previous one's output. In TypeScript 6.0 each phase is one file in src/compiler/; the Go port keeps the design as packages under internal/.Figure 7.1: The TypeScript compiler pipeline
  • The scanner turns characters into tokens (interface, Item, {), skipping whitespace and comments.

  • The parser builds an abstract syntax tree (AST) of nodes such as InterfaceDeclaration, recovering from syntax errors so your editor keeps working.

  • The binder creates a symbol for each declaration, links names to scopes and builds the control-flow graph that narrowing (Section 7.4) uses.

  • The checker computes types lazily, when something asks, and compares them. At 54,434 lines in 6.0, checker.ts holds most of the language.

  • The emitter erases types, downlevels syntax for older target values and writes JavaScript, declaration files and source maps.

  • --extendedDiagnostics reports the time spent in each phase:

    $ npx tsc --extendedDiagnostics --outDir dist cart.ts | grep -E "Files|Memory used|time"

    Files: 83

    Memory used: 65514K

    Parse time: 0.033s

    Bind time: 0.011s

    Check time: 0.238s

    Emit time: 0.002s

    Total time: 0.286s

    Listing 7.4: Timing the phases with TypeScript 7.0 The 11-line cart.ts pulls in 82 standard library files (lib.es2025.d.ts, lib.dom.d.ts and more), and checking dominates. TypeScript 6.0 needed 0.98 s and 149 MB for the same job. Because checking is the costly phase, TypeScript 7.0 spreads files across parallel checker workers.

    7.1.4 Installing and Running

    Install the compiler per project so everyone uses the same version: npm install -D typescript, then npx tsc --init writes a starter tsconfig.json (Section 7.12). npx tsc checks the project and emits JavaScript; npx tsc --noEmit only checks. You can also skip the build step and run .ts files directly, because several runtimes now strip types on the fly (runtimes are introduced in Section 6.1.2): Tool Run a file Type-checks? How it works tsc 7.0.2 npx tsc Yes Emits .js files tsx 4.23 npx tsx app.ts No Node.js plus esbuild; tsx watch Node.js 22.18+ node app.ts No Blanks out types with spaces Deno 2.9 deno run app.ts With --check Bundles TypeScript 6.0.3 for deno check Bun 1.4 bun app.ts No Native transpiler Table 7.2: Ways to run TypeScript (September 2026) Node.js enables type stripping by default since 23.6 and 22.18, and marked it stable in 25.2 and 24.12. It replaces annotations with whitespace, so line and column numbers stay correct without source maps, and it refuses syntax that would need generated JavaScript: enum, namespace with code, parameter properties and import = aliases. It also ignores tsconfig.json and needs import type and full file extensions (./cart.ts). Here one enum meets four runtimes:

    $ node status.ts 2>&1 | grep SyntaxError

    SyntaxError [ERR_UNSUPPORTED_TYPESCRIPT_SYNTAX]: TypeScript enum is not supported in strip-only mode

    $ npx tsx status.ts

    1

    $ deno run status.ts

    1

    $ bun status.ts

    1

    Listing 7.5: An enum under Node.js, tsx, Deno and Bun status.ts declares enum Status { Active, Archived } and logs Status.Archived. Set "erasableSyntaxOnly": true and "verbatimModuleSyntax": true in tsconfig.json so tsc flags such syntax (error TS1294) before Node.js does. Decorators also survive stripping, and Node.js cannot run them yet (Section 6.7.11), while tsx and Deno transform them. Warning: Only tsc and deno check find type errors. Run cart.ts from Section 7.1.1 with the quantity typo under Node.js, tsx, deno run or Bun and each prints NaN. Keep tsc --noEmit in your editor, pre-commit hook and CI pipeline, and let the runtime just run the code.

    7.2 The Basics

    The compiler tsc does two independent jobs: it checks your program against the types it can see, and it emits JavaScript with the types removed. Keeping those jobs apart in your head explains most of TypeScript's behavior.In JavaScript, user.nmae is simply undefined and Math.random < 0.5 quietly compares a function with a number (Section 6.2.3). The checker reads the source without running it and reports such non-exception failures.

    const user = { name: "Ada", born: 1815 };

    function age(person: { born: number }, year) {

    return year - person.born;

    }

    console.log(user.nmae); // typo

    if (Math.random < 0.5) console.log("heads"); // forgot the ()

    const box = document.querySelector("input"); // may be null

    box.value = String(age(user, 2026));

    Listing 7.6: Four bugs that JavaScript runs silently and tsc rejects

    basics.ts(3,40): error TS7006: Parameter 'year' implicitly has an 'any' type.

    basics.ts(6,18): error TS2339: Property 'nmae' does not exist on type '{ name: string; born:

    number; }'.

    basics.ts(7,5): error TS2365: Operator '<' cannot be applied to types '() => number' and

    'number'.

    basics.ts(9,1): error TS18047: 'box' is possibly 'null'.

    Output of Listing 7.6 Run npx tsc basics.ts and you still get basics.js, with the annotations deleted and "use strict"; on top: tsc emits despite errors unless you pass --noEmitOnError. Erased types never change what the program does, and you cannot test a type at runtime; narrowing (Section 7.4) relies on ordinary JavaScript checks. Erasure is also why Node.js can run .ts files directly (Section 7.1.4); --erasableSyntaxOnly rejects the few constructs that emit code, such as enum and constructor parameter properties.The target option names the ECMAScript version the output may use; newer syntax is rewritten. Since TypeScript 6.0 the default is es2025, so modern code passes through unchanged. ES2015 is now the floor: TypeScript 7 answers --target es5 with error TS5108: Option 'target=ES5' has been removed.

    export function retries(opts?: { max?: number }): number {

    const max = opts?.max;

    return max ?? 2 ** 3;

    }

    Listing 7.7: Optional chaining, ?? and ** compiled with --target es2015

    export function retries(opts) {

    const max = opts === null || opts === void 0 ? void 0 : opts.max;

    return max !== null && max !== void 0 ? max : Math.pow(2, 3);

    }

    Output of Listing 7.7 Downleveling rewrites syntax only; it does not add missing library functions such as Array.prototype.findLast.strict, on by default since TypeScript 6.0, switches on nine checks, chiefly noImplicitAny (the first error above) and strictNullChecks (the last); with --strict false only the middle two errors remain. Keep it on for new code; when migrating JavaScript, relax single flags in tsconfig.json (Section 7.12) and restore them gradually.

    7.3 Everyday Types

    Each type describes a set of the JavaScript values from Chapter 6: number is every number, "GET" is one string, string | null is every string plus null. A small vocabulary covers most code. Type Example annotation Meaning string, number, boolean, bigint, symbol let price: number = 9.5 Lowercase; no separate int or float null, undefined let el: Element | null Separate types under strictNullChecks Array string[] or Array<string> [string, number] is a tuple (Section 7.6) Object type { x: number; y?: number } ? marks an optional property Union number | string A value of either type Literal "left" | "right" Exactly these values Function (n: number) => string Parameters and result (Section 7.5) void function log(): void Returns no useful value any let raw: any Opts out of checking unknown let input: unknown Anything; must be narrowed before use never function fail(): never No value: throws or cannot happen Table 7.3: Everyday TypeScript types An annotation follows the name after a colon. Variables rarely need one: TypeScript infers the type from the initializer and keeps it, so a let that starts as a number stays a number. Annotate function parameters, and the return type of exported functions to document intent. Callbacks need nothing: contextual typing gives tag in tags.forEach((tag) => ...) the array's element type.

    let title: string = "TypeScript"; // annotation (redundant here)

    let count = 42; // inferred: number

    const tags = ["ts", "js"]; // inferred: string[]

    function average(values: number[]): number {

    return values.reduce((sum, v) => sum + v, 0) / values.length;

    }

    tags.forEach((tag) => console.log(tag.toUpperCase())); // tag: string

    count = "many";

    average(tags);

    title.toUppercase();

    Listing 7.8: Inferred variables, annotated parameters, contextually typed callbacks

    everyday.ts(10,1): error TS2322: Type 'string' is not assignable to type 'number'.

    everyday.ts(11,9): error TS2345: Argument of type 'string[]' is not assignable to parameter of

    type 'number[]'.

    Type 'string' is not assignable to type 'number'.

    everyday.ts(12,7): error TS2551: Property 'toUppercase' does not exist on type 'string'. Did

    you mean 'toUpperCase'?

    Output of Listing 7.8 An object type lists property names and their types; ? makes a property optional (its type gains undefined) and readonly forbids reassignment. A union A | B accepts either member, but you may only use operations valid for every member, so id.toFixed() fails when id might be a string. Give any type a reusable name with a type alias, or name an object shape with an interface.

    type Id = number | string; // type alias for a union

    interface User {

    id: Id;

    name: string;

    email?: string; // optional: string | undefined

    readonly createdAt: Date;

    }

    interface User { roles: string[] } // interfaces merge; aliases cannot

    function label(user: User): string {

    const mail = user.email.toLowerCase();

    return `${user.name} #${user.id.toFixed(0)}`;

    }

    const ada: User = { id: 1, name: "Ada", createdAt: new Date(), roles: [], age: 36 };

    ada.createdAt = new Date();

    type Id = bigint;

    Listing 7.9: An interface, a union alias, optional and readonly properties

    objects.ts(1,6): error TS2300: Duplicate identifier 'Id'.

    objects.ts(12,16): error TS18048: 'user.email' is possibly 'undefined'.

    objects.ts(13,35): error TS2339: Property 'toFixed' does not exist on type 'Id'.

    Property 'toFixed' does not exist on type 'string'.

    objects.ts(16,75): error TS2353: Object literal may only specify known properties, and 'age'

    does not exist in type

    'User'.

    objects.ts(17,5): error TS2540: Cannot assign to 'createdAt' because it is a read-only

    property.

    objects.ts(18,6): error TS2300: Duplicate identifier 'Id'.

    Output of Listing 7.9 TypeScript is structurally typed: any object with the right properties is a User. The age error is an excess property check, applied only to object literals written in place, where an extra property is usually a typo. Narrowing (Section 7.4) fixes the email and id errors. interface type alias Names Object shapes only Any type: unions, tuples, primitives Extending interface B extends A {} Intersection type B = A & {} Re-opening Declarations merge Duplicate identifier error Table 7.4: Interfaces versus type aliases The handbook's rule of thumb: use interface until you need a feature only type offers. Merging is how libraries let you add properties to globals such as Window (Section 7.10).A const holding "GET" has the literal type "GET", but a let or an object property is widened to string because it could change later. To keep an object's literal types, add as const, which also makes it readonly.

    type Method = "GET" | "POST";

    declare function send(url: string, method: Method): void;

    let loose = "GET"; // widened to string

    const req = { url: "/api", method: "GET" };

    const frozen = { url: "/api", method: "GET" } as const;

    send(req.url, loose);

    send(req.url, req.method);

    send(frozen.url, frozen.method); // OK: method is "GET"

    const canvas = document.getElementById("chart") as HTMLCanvasElement;

    const input = document.querySelector("input")!; // trust me: not null

    const n = "42" as number;

    const data: unknown = JSON.parse('{"x":1}');

    data.x;

    const risky: any = JSON.parse('{"x":1}');

    risky.y.z(); // no error, crashes at runtime

    Listing 7.10: Widening, literal unions, as const, assertions, any and unknown

    literals.ts(8,15): error TS2345: Argument of type 'string' is not assignable to parameter of

    type 'Method'.

    literals.ts(9,15): error TS2345: Argument of type 'string' is not assignable to parameter of

    type 'Method'.

    literals.ts(14,11): error TS2352: Conversion of type 'string' to type 'number' may be a

    mistake because neither type

    sufficiently overlaps with the other. If this was intentional, convert the expression to

    'unknown' first.

    literals.ts(17,1): error TS18046: 'data' is of type 'unknown'.

    Output of Listing 7.10 A type assertion expr as T tells the checker you know better. getElementById returns HTMLElement | null, and only you know that #chart is a canvas. Assertions are erased, so nothing checks them at runtime, and TypeScript allows them only toward a more or less specific type: "42" as number is rejected. The postfix ! is a shorthand assertion that removes null and undefined. Both are promises you must keep.any switches checking off in both directions and spreads silently: JSON.parse returns any, so risky.y.z() compiles and throws a TypeError when it runs. Annotate such values as unknown instead; the checker then refuses every operation until a typeof, in or custom guard proves what the value is. Tip: Treat each as, ! and any as a code-review flag. typescript-eslint (Section 7.13) can forbid explicit any and non-null assertions.

    7.4 Narrowing

    A union type says what a value might be; before you call toFixed or trim you must prove which member you have. TypeScript follows every if, return, switch, && and assignment in a function (control flow analysis) and gives a variable a narrower type in each branch that ordinary JavaScript checks make safe. Nothing is added at runtime: the checks are the ones Section 6.4.1 already describes, and the compiler reads them. Guard Example In the true branch typeof typeof x === "string" string (also number, bigint, boolean, symbol, undefined, object, function) Truthiness if (x) Removes null, undefined and falsy literals Equality x == null, x === "a" null | undefined, or the literal in "swim" in pet Members that have (or may have) swim instanceof x instanceof Date Date Type predicate (x: unknown): x is Cat Whatever the predicate names Table 7.5: Narrowing guards Figure 7.2: Control flow analysis narrows value at each check in describe()Each return removes a member from the rest of the function, so by the last line of describe only number is left. A type predicate such as x is string lets your own function act as a guard; since TypeScript 5.5 the compiler also infers one for simple arrow functions, so filter((n) => n !== undefined) returns number[].

    function describe(value: string | number | Date | null | undefined): string {

    if (value == null) return "nothing"; // null | undefined

    if (typeof value === "string") return `text "${value.trim()}"`;

    if (value instanceof Date) return `year ${value.getFullYear()}`;

    return `number ${value.toFixed(1)}`; // only number is left

    }

    const isText = (x: unknown): x is string => typeof x === "string";

    const mixed: unknown[] = ["a", 1, "b", null];

    const texts = mixed.filter(isText); // string[]

    const counts = [3, undefined, 0].filter((n) => n !== undefined); // number[]

    const label = (width?: number) => (width ? `${width}px` : "auto"); // 0 is falsy!

    console.log(describe(null), describe(" hi "), describe(new Date(2026, 8)), describe(Math.PI));

    console.log(texts, counts);

    console.log(label(120), label(undefined), label(0));

    Listing 7.11: typeof, ==, instanceof, a type predicate and a truthiness trap

    nothing text "hi" year 2026 number 3.1

    [ 'a', 'b' ] [ 3, 0 ]

    120px auto auto

    Output of Listing 7.11 The last line shows the classic truthiness bug: label(0) prints auto because 0 is falsy. The narrowing is sound, so the checker accepts it; test width === undefined instead. Likewise typeof x === "object" leaves null in the type, because typeof null is "object" (Section 6.2.3).The most useful pattern models state as a union of object types sharing a literal discriminant property. Checking it narrows the whole object, so each branch sees only the fields of that state: you cannot read data while loading. Reducers, API results and UI state machines are typed this way.

    type FetchState =

    | { status: "loading"; startedAt: number }

    | { status: "success"; data: string[] }

    | { status: "error"; error: Error; retries: number };

    function render(state: FetchState): string {

    switch (state.status) { // status is the discriminant

    case "loading":

    return `Loading since ${state.startedAt}`;

    case "success":

    return state.data.join(", "); // state: { status: "success"; data: string[] }

    case "error":

    return `${state.error.message} (retry ${state.retries})`;

    default:

    return assertNever(state); // state: never

    }

    }

    function assertNever(value: never): never {

    throw new Error(`Unhandled state: ${JSON.stringify(value)}`);

    }

    console.log(render({ status: "success", data: ["ts", "js"] }));

    console.log(render({ status: "error", error: new Error("Timeout"), retries: 2 }));

    Listing 7.12: A discriminated union with an exhaustive switch

    ts, js

    Timeout (retry 2)

    Output of Listing 7.12 After three cases nothing is left, so state has type never, and never accepts no value but itself. Add | { status: "cancelled" } to the union and forget the case, and the build fails at the default line with error TS2345: Argument of type '{ status: "cancelled"; }' is not assignable to parameter of type 'never'. The assertNever call also throws if untyped data slips through at runtime. Tip: Narrowing works on values the compiler can track. Copy a property into a const before a callback, and prefer a discriminant over optional fields (data?: string[], error?: Error), which allow impossible combinations such as both at once.

    7.5 More on Functions

    Functions are values in JavaScript: you pass them as callbacks, store them in objects and call them with a borrowed this (Section 6.5). TypeScript gives each of those roles a type. Syntax Example Use it for Function type expression (value: number) => string Callbacks, function variables Call signature { (s: string): boolean; message: string } Callable objects with properties Construct signature new (x: number) => Point Classes passed as values Optional, default, rest (p?: string, g = "Hi", ...n: number[]) Arguments callers may omit Type parameter <T>(items: T[]) => T Relating inputs to outputs this parameter function (this: Counter) Functions that rely on this Table 7.6: Ways to type a function A function type expression looks like an arrow function with types. The parameter name is required but plays no part in compatibility. When a function also carries properties, write a call signature inside an object type (note the colon instead of the arrow); prefix it with new for a construct signature, the type of a class.

    type Formatter = (value: number) => string;

    type Validator = {

    message: string; // a property...

    (input: string): boolean; // ...plus a call signature

    };

    const euro: Formatter = (v) => `€${v.toFixed(2)}`; // v: number from context

    function isEmail(s: string) { return s.includes("@"); }

    isEmail.message = "Enter a valid email address"; // expando property

    const check: Validator = isEmail; // OK

    const bad: Formatter = (v: string) => v;

    Listing 7.13: A function type expression and a call signature

    sigs.ts(11,7): error TS2322: Type '(v: string) => string' is not assignable to type

    'Formatter'.

    Types of parameters 'v' and 'value' are incompatible.

    Type 'number' is not assignable to type 'string'.

    Output of Listing 7.13 Property assignments on a function declaration become part of its type, so isEmail satisfies Validator. Avoid the global type Function: calling it returns any.JavaScript lets callers omit any argument (Section 6.5.1); TypeScript does not unless you say so. punct?: string has the type string | undefined, whereas a default value makes the parameter optional and keeps it a plain string in the body. A rest parameter has an array type, and spreading into a fixed parameter list needs a tuple, which as const produces.

    function greet(name: string, greeting = "Hello", punct?: string): string {

    return `${greeting}, ${name}${punct ?? "!"}`; // punct: string | undefined

    }

    function sum(first: number, ...rest: number[]): number {

    return rest.reduce((acc, n) => acc + n, first);

    }

    const pair = [8, 5] as const; // readonly [8, 5]

    const loose = [8, 5]; // number[]

    console.log(greet("Ada"), sum(1, 2, 3), Math.atan2(...pair).toFixed(3));

    greet();

    Math.atan2(...loose);

    const lengths: number[] = [];

    ["a", "bb"].forEach((s) => lengths.push(s.length)); // push returns a number: OK

    function reset(): void { return lengths.length; }

    Listing 7.14: Optional, default and rest parameters, spreads and void

    params.ts(11,1): error TS2554: Expected 1-3 arguments, but got 0.

    params.ts(12,12): error TS2556: A spread argument must either have a tuple type or be passed

    to a rest

    parameter.

    params.ts(16,26): error TS2322: Type 'number' is not assignable to type 'void'.

    Output of Listing 7.14 Without the error lines the program prints Hello, Ada! 6 1.012. The last two lines show a deliberate asymmetry: a contextual void return type, like the callback type of forEach, means "the result is ignored", so returning push's number is fine; a function declared : void must not return a value. Warning: Never mark a callback parameter optional because some callbacks ignore it. A callback with fewer parameters is already assignable ((s) => ... fits (s: string, i: number) => void), and i?: number would force every implementer to check for undefined. When the output type depends on the input type, add a type parameter. TypeScript infers it at each call, and a constraint such as T extends { length: number } limits what callers may pass while letting the body use .length. Use as few type parameters as possible; one that appears only once in the signature is probably unnecessary. Section 7.7.1 covers generics in depth.

    function first<T>(items: T[]): T | undefined { return items[0]; }

    function longest<T extends { length: number }>(a: T, b: T): T {

    return a.length >= b.length ? a : b;

    }

    const n = first([3, 1, 2]); // number | undefined

    const word = longest("tea", "coffee"); // T is string

    longest(10, 20);

    Listing 7.15: Inferred type arguments and a constraint

    generic.ts(7,9): error TS2345: Argument of type 'number' is not assignable to parameter of type

    '{ length: number; }'.

    Output of Listing 7.15 Some functions accept genuinely different argument lists. Write two or more overload signatures followed by one implementation signature whose body handles every case. Callers see only the overloads.

    function parseDate(timestamp: number): Date;

    function parseDate(year: number, month: number, day: number): Date;

    function parseDate(iso: string): Date;

    function parseDate(a: number | string, month?: number, day?: number): Date {

    if (typeof a === "string") return new Date(a);

    return month !== undefined && day !== undefined ? new Date(a, month - 1, day) : new Date(a);

    }

    console.log(parseDate(2026, 9, 15).getMonth()); // 8 (months count from 0)

    parseDate(2026, 9);

    declare const input: number | string;

    parseDate(input);

    Listing 7.16: Three overloads over one hidden implementation

    overload.ts(10,1): error TS2575: No overload expects 2 arguments, but overloads do exist that

    expect either

    1 or 3 arguments.

    overload.ts(12,11): error TS2769: No overload matches this call.

    The last overload gave the following error.

    Argument of type 'string | number' is not assignable to parameter of type 'string'.

    Type 'number' is not assignable to type 'string'.

    Output of Listing 7.16 The second error is the classic pitfall: each call is checked against one overload at a time, so a number | string argument matches none of them although the implementation would accept it. Prefer a union parameter when the result type is the same; keep overloads for when arguments decide the result type, as in createElement, which returns HTMLCanvasElement for "canvas" but HTMLElement for an unknown tag.In a function (not an arrow), this depends on how the function is called (Section 6.3.10). Declare its type as a fake first parameter named this: it is erased from the JavaScript output, and every call site is checked. Under noImplicitThis, part of strict, an untyped this outside a method is an error.

    interface Counter { count: number; step: number }

    function increment(this: Counter, times = 1): number {

    this.count += this.step * times;

    return this.count;

    }

    const counter = { count: 0, step: 5, increment };

    counter.increment(2); // OK: this is counter

    const detached = counter.increment;

    detached();

    document.querySelector("button")?.addEventListener("click", function () {

    this.disabled = true; // this: HTMLButtonElement

    });

    function legacy() { return this.count; }

    Listing 7.17: A this parameter, a detached method and a typed event handler

    this.ts(9,1): error TS2684: The 'this' context of type 'void' is not assignable to method's

    'this' of type

    'Counter'.

    this.ts(14,28): error TS2683: 'this' implicitly has type 'any' because it does not have a type

    annotation.

    Output of Listing 7.17 The DOM library declares listeners with a this: HTMLButtonElement parameter, so a function handler gets a typed this for free. An arrow function captures the surrounding this instead.

    7.6 Object Types

    Section 7.3 introduced object types, interface and type. The details below decide whether an object type really protects you: how far readonly reaches, what dictionaries return for missing keys, when extra properties are rejected and how types combine. Every rule is about the shape of a value, never the constructor that made it.readonly stops reassignment of the property itself but is shallow: nested objects stay mutable. A readonly string[] (or ReadonlyArray<string>) removes push, splice and index assignment. It is a compile-time promise only, and assignability ignores it, so a mutable alias can still change the property; use Object.freeze (Section 6.3.11) for runtime protection.

    interface Settings {

    readonly theme: { dark: boolean };

    readonly tags: readonly string[];

    fontSize?: number; // number | undefined

    }

    const s: Settings = { theme: { dark: false }, tags: ["ts"] };

    s.theme.dark = true; // OK: readonly is shallow

    s.theme = { dark: true };

    s.tags.push("js");

    const writable: { theme: { dark: boolean } } = s; // readonly ignored here

    writable.theme = { dark: false }; // changes s.theme too

    s.fontSize = undefined; // allowed by default

    Listing 7.18: Shallow readonly, a mutable alias and an optional property

    modifiers.ts(8,3): error TS2540: Cannot assign to 'theme' because it is a read-only property.

    modifiers.ts(9,8): error TS2339: Property 'push' does not exist on type 'readonly string[]'.

    Output of Listing 7.18 An optional property may be missing or hold undefined. The exactOptionalPropertyTypes option (not part of strict) forbids writing undefined explicitly and reports error TS2412 on the last line, which matters when code tests "fontSize" in s or merges objects with spread.When property names are not known in advance, as in a lookup table or parsed JSON, an index signature [key: string]: T describes every property at once; Record<string, T> is the same type as a utility (Section 7.7.7). Keys may be string, number, symbol, template-literal patterns or unions of these. Named properties must fit the signature, and a numeric index type must be assignable to the string one, because JavaScript converts obj[1] to obj["1"].

    interface HttpHeaders {

    [name: string]: string; // any string key, string values

    "content-type": string; // known properties must fit the signature

    retries: number;

    }

    const stock: Record<string, number> = { apples: 3 }; // same as { [k: string]: number }

    const pears = stock.pears; // number, although no pears exist

    console.log(pears.toFixed(0));

    type Grid = { [cell: `${string}-${number}`]: boolean }; // template-pattern keys

    const grid: Grid = { "A-1": true, B2: false };

    Listing 7.19: Index signatures, Record and template-pattern keys

    index.ts(4,3): error TS2411: Property 'retries' of type 'number' is not assignable to 'string'

    index type 'string'.

    index.ts(11,35): error TS2353: Object literal may only specify known properties, and 'B2' does

    not exist in type

    'Grid'.

    Output of Listing 7.19 The line that compiles is the dangerous one: stock.pears is typed number but is undefined at runtime, so toFixed throws a TypeError. With noUncheckedIndexedAccess every index read, array elements included, gains | undefined, and tsc reports error TS18048: 'pears' is possibly 'undefined'. For dictionaries that change at runtime, a Map<string, number> is often clearer: its get already returns number | undefined.Structural typing normally allows extra properties. The exception is a fresh object literal written directly where a type is expected: an unknown property there is almost certainly a typo. Assigning the literal to a variable first, asserting its type or adding an index signature switches the check off.

    interface ButtonOptions {

    label: string;

    color?: "primary" | "danger";

    }

    declare function button(options: ButtonOptions): HTMLButtonElement;

    button({ label: "Save", colour: "primary" }); // typo caught

    const opts = { label: "Save", colour: "primary" };

    button(opts); // no check: not a fresh literal

    const theme = {

    save: { label: "Save", color: "primary" },

    drop: { label: "Delete", color: "danger", icon: "trash" },

    } satisfies Record<string, ButtonOptions>;

    const c: "primary" = theme.save.color; // OK: literal type kept

    Listing 7.20: Fresh literals are checked, variables are not, satisfies keeps literal types

    excess.ts(7,25): error TS2561: Object literal may only specify known properties, but 'colour'

    does not exist in type

    'ButtonOptions'. Did you mean to write 'color'?

    excess.ts(13,45): error TS2353: Object literal may only specify known properties, and 'icon'

    does not exist in type

    'ButtonOptions'.

    Output of Listing 7.20 The second call compiles and silently ignores colour. The satisfies operator (TypeScript 4.9) checks a literal, excess properties included, without replacing its inferred type, so theme.save.color stays "primary" and a misspelled theme.sav is an error, which a Record annotation would allow. Prefer it to an annotation for configuration objects.interface B extends A, C combines one or more types and checks that redeclared properties stay compatible. An intersection A & C builds the same combination but never complains: conflicting properties are intersected, and number & string is never, a type no value has.

    interface Entity { id: number }

    interface Timestamped { createdAt: Date }

    interface Post extends Entity, Timestamped { title: string }

    type Reply = Entity & Timestamped & { body: string };

    const reply: Reply = { id: 2, createdAt: new Date(), body: "Nice" };

    interface Draft extends Post { id: string }

    type Broken = Post & { id: string }; // id: number & string = never

    const b: Broken = { id: 3, createdAt: new Date(), title: "x" };

    Listing 7.21: Interface extension reports conflicts; intersections produce never

    extend.ts(7,11): error TS2430: Interface 'Draft' incorrectly extends interface 'Post'.

    Types of property 'id' are incompatible.

    Type 'string' is not assignable to type 'number'.

    extend.ts(9,21): error TS2322: Type 'number' is not assignable to type 'never'.

    Output of Listing 7.21 The interface reports the mistake where you made it; the intersection defers it to a puzzling error at first use. The TypeScript team's performance guide also favors extends over large intersections, because the compiler caches relationships between named interfaces. Feature interface type alias Unions, tuples, primitives, mapped types No Yes Combining object types extends: conflicts are errors &: conflicts become never Declaration merging Yes, reopen to add members No, duplicate identifier implements in a class Yes Object types only, not unions Table 7.7: Choosing between interface and type Object types take type parameters too: interface Box<T> { contents: T } works like Array<T> or Map<K, V> (Section 7.7.1). A tuple is an array with a fixed length and a type per position. Elements can be labeled, optional (only at the end, making length a union) or rest elements, and as const infers a readonly tuple.

    type Span = [start: number, end: number, step?: number]; // labeled, optional

    type Row = [id: number, ...cells: string[]]; // rest element

    const r: Span = [0, 10];

    const row: Row = [7, "Ada", "Lovelace"];

    const len: 2 | 3 = r.length;

    const point = [3, 4] as const; // readonly [3, 4]

    function total(values: readonly number[]) {

    values[0] = 1;

    }

    const short: Span = [1];

    Listing 7.22: Tuples and readonly array parameters

    tuple.ts(9,3): error TS2542: Index signature in type 'readonly number[]' only permits reading.

    tuple.ts(11,7): error TS2322: Type '[number]' is not assignable to type 'Span'.

    Source has 1 element(s) but target requires 2.

    Output of Listing 7.22 Declare readonly T[] parameters in functions that only read: a mutable array is assignable to them but not the reverse, so callers can pass either. Keep tuples for short positional data such as [key, value] entries; beyond three elements, an object with named properties reads better.

    7.7 Type Manipulation

    TypeScript lets you compute new types from existing types and values. Instead of writing a UserUpdate type by hand and keeping it in sync with User, you derive it: Partial<Omit<User, "id">>. When User gains a field, every derived type follows, and the compiler flags each place that now needs attention.The type-level operators mirror JavaScript operations you already know from Chapter 6. They run only in the checker and are erased before the code runs (Section 7.2): Type operator Example JavaScript analogy Generic parameter Box<T> Function parameter keyof keyof User Object.keys(user) typeof (type context) typeof config Reading a variable Indexed access User["email"] user["email"] Conditional type T extends string ? A : B cond ? a : b (Section 6.22.2) Mapped type { [K in keyof T]: ... } for...in building an object Template literal type ` on${Capitalize<E>} ` Template string Table 7.8: Type operators and their JavaScript counterparts

    7.7.1 Generics

    A generic is a type with parameters. Array<T>, Promise<T> and Map<K, V> work for any element type while keeping that type connected from input to output. You declare type parameters in angle brackets and usually let TypeScript infer the arguments from the call (Section 7.5). A parameter can carry a constraint (<K extends keyof T>), a default (<T = string>), or, since TypeScript 5.0, the const modifier, which infers literal types without as const. In a typed event emitter, each instance receives a map of event names to payload types, and every on and emit call is checked against it:

    type Listener<T> = (payload: T) => void;

    class Emitter<Events extends Record<string, unknown>> {

    private listeners: { [E in keyof Events]?: Listener<Events[E]>[] } = {};

    on<E extends keyof Events>(event: E, listener: Listener<Events[E]>): void {

    (this.listeners[event] ??= []).push(listener);

    }

    emit<E extends keyof Events>(event: E, payload: Events[E]): void {

    this.listeners[event]?.forEach((listener) => listener(payload));

    }

    }

    type CartEvents = { added: { sku: string; qty: number }; cleared: undefined };

    const cart = new Emitter<CartEvents>();

    cart.on("added", (item) => console.log(`${item.qty} x ${item.sku}`));

    cart.emit("added", { sku: "TS-BOOK", qty: 2 });

    cart.emit("added", { sku: "TS-BOOK" });

    cart.on("removed", () => {});

    Listing 7.23: A typed event emitter

    $ npx tsc --noEmit

    emitter.ts(17,20): error TS2741: Property 'qty' is missing in type '{ sku: string; }' but

    required in type '{ sku: string; qty: number; }'.

    emitter.ts(18,9): error TS2345: Argument of type '"removed"' is not assignable to parameter of

    type 'keyof CartEvents'.

    $ node emitter.ts

    2 x TS-BOOK

    undefined x TS-BOOK

    Output of Listing 7.23 The callback's item needs no annotation: E is inferred as "added", so Events[E] is the payload type. Node.js still runs the faulty call and prints undefined, because generics are erased like every other type. Warning: Declare CartEvents with type, not interface. An interface has no implicit index signature, so it fails the Record<string, unknown> constraint with error TS2344.

    7.7.2 The keyof and typeof Operators

    keyof T produces the union of T's property names as literal types. In a type position, typeof x produces the type TypeScript inferred for the variable x. Combined, they turn an ordinary object into the single source of truth for its keys and value types, as in this settings reader backed by localStorage (Section 9.3.2):

    const defaults = { theme: "light", pageSize: 20, beta: false };

    type Settings = typeof defaults; // { theme: string; pageSize: number; beta: boolean }

    type SettingKey = keyof Settings; // "theme" | "pageSize" | "beta"

    function readSetting<K extends SettingKey>(key: K): Settings[K] {

    const stored = localStorage.getItem(key);

    return stored === null ? defaults[key] : JSON.parse(stored);

    }

    const size = readSetting("pageSize"); // number

    size.toFixed(0);

    readSetting("colour");

    const saved: Settings = { theme: "dark", pageSize: "50", beta: true };

    Listing 7.24: Deriving types from a defaults object

    settings.ts(10,13): error TS2345: Argument of type '"colour"' is not assignable to parameter

    of type '"beta" | "pageSize" | "theme"'.

    settings.ts(11,42): error TS2322: Type 'string' is not assignable to type 'number'.

    Output of Listing 7.24 Add a setting to defaults and every derived type follows (tsc 7.0 may list union members in a different order from your source). For an index signature, keyof { [k: string]: boolean } is string | number, because JavaScript coerces numeric keys to strings. Object literal properties widen ("light" becomes string); add as const when you need the literal. Warning: JavaScript's typeof (Chapter 6) runs at runtime and returns a string such as "object"; TypeScript's typeof appears only in type positions and is erased. It accepts only identifiers and property paths, so typeof readSetting("beta") is a syntax error; write ReturnType<typeof readSetting> instead.

    7.7.3 Indexed Access Types

    An indexed access type T[K] looks up the type of a property with the bracket syntax you use to read a value. The index can be a literal, a union of keys, keyof T (giving the union of all value types), or number to get an array's element type. It reaches into large generated types, such as API client responses, without copying nested shapes:

    type Order = {

    id: string;

    customer: { name: string; address: { city: string; zip: string } };

    items: { sku: string; price: number }[];

    };

    type Address = Order["customer"]["address"]; // { city: string; zip: string }

    type LineItem = Order["items"][number]; // { sku: string; price: number }

    const ROLES = ["admin", "editor", "viewer"] as const;

    type Role = (typeof ROLES)[number]; // "admin" | "editor" | "viewer"

    const line: LineItem = { sku: "A1", price: "9.99" };

    const role: Role = "owner";

    Listing 7.25: Extracting nested types from an order

    order.ts(10,37): error TS2322: Type 'string' is not assignable to type 'number'.

    order.ts(11,7): error TS2322: Type '"owner"' is not assignable to type '"admin" | "editor" |

    "viewer"'.

    Output of Listing 7.25 The ROLES pattern is worth memorizing: one runtime array feeds both a <select> menu and the Role type, so the two cannot drift apart. Warning: Dot syntax and variables do not work in type positions. Order.customer fails with error TS2713: Cannot access 'Order.customer' because 'Order' is a type, but not a namespace, and Order[k] for a const k fails with TS2749; write Order[typeof k].

    7.7.4 Conditional Types

    A conditional type T extends U ? X : Y is the type-level ternary: if T is assignable to U, the result is X, otherwise Y. Inside the condition, infer Name declares a type variable that TypeScript fills in by matching. Conditional types arrived in TypeScript 2.8; since 4.1 they may refer to themselves:

    type Unwrap<T> = T extends Promise<infer V> ? Unwrap<V> : T; // recursive

    async function fetchUser(id: string) {

    const res = await fetch(`/api/users/${id}`);

    return (await res.json()) as { id: string; name: string };

    }

    type User = Unwrap<ReturnType<typeof fetchUser>>; // { id: string; name: string }

    const guest: User = { id: "0" };

    type ToArray<T> = T extends unknown ? T[] : never; // distributive

    type ToArrayOnce<T> = [T] extends [unknown] ? T[] : never;

    const mixed: ToArrayOnce<string | number> = ["a", 1];

    const split: ToArray<string | number> = ["a", 1];

    Listing 7.26: Unwrapping a promise with infer, with and without distribution

    api.ts(7,7): error TS2741: Property 'name' is missing in type '{ id: string; }' but required

    in type '{ id: string; name: string; }'.

    api.ts(12,7): error TS2322: Type '(string | number)[]' is not assignable to type 'string[] |

    number[]'.

    Type '(string | number)[]' is not assignable to type 'string[]'.

    Type 'string | number' is not assignable to type 'string'.

    Type 'number' is not assignable to type 'string'.

    Output of Listing 7.26 When the checked type is a bare type parameter, the condition distributes: it runs once per union member and joins the results, so ToArray<string | number> is string[] | number[] and rejects a mixed array. Brackets, [T] extends [unknown], switch distribution off. Distribution makes filters work: the built-in Exclude<T, U> is just T extends U ? never : T, because never vanishes from a union. Tip: The built-in Awaited<T> already does what Unwrap does (Section 7.7.7). Recursion is capped; a type that nests too deeply fails with error TS2589: Type instantiation is excessively deep and possibly infinite.

    7.7.5 Mapped Types

    A mapped type builds an object type by looping over keys: { [K in keyof T]: NewType } visits each property of T the way for...in visits an object, with T[K] as the original property type. The modifiers readonly and ? can be added with + (the default) or removed with -. Partial, Required and Readonly are one-line mapped types; add a conditional type and recursion and you can make a whole configuration tree immutable:

    type DeepReadonly<T> = {

    readonly [K in keyof T]: T[K] extends object ? DeepReadonly<T[K]> : T[K];

    };

    type Flags<T> = { [K in keyof T]-?: boolean }; // -? removes optionality

    type AppConfig = { api: { baseUrl: string; retries: number }; hosts: string[]; debug?: boolean };

    const config: DeepReadonly<AppConfig> = { api: { baseUrl: "/api", retries: 3 }, hosts: [] };

    config.api.retries = 5;

    config.hosts.push("b.example.com");

    const shown: Flags<AppConfig> = { api: true, hosts: false };

    Listing 7.27: DeepReadonly and a modifier-removing mapped type

    config.ts(7,12): error TS2540: Cannot assign to 'retries' because it is a read-only property.

    config.ts(8,14): error TS2339: Property 'push' does not exist on type 'readonly string[]'.

    config.ts(9,7): error TS2741: Property 'debug' is missing in type '{ api: true; hosts: false;

    }' but required in type 'Flags<AppConfig>'.

    Output of Listing 7.27 Built-in Readonly<AppConfig> would block only config.api = ...; the recursion reaches retries, and mapping over an array type yields readonly string[].Since TypeScript 4.1 an as clause renames or drops keys while mapping, and a key mapped to never disappears: { [K in keyof T as K extends "password" ? never : K]: T[K] } strips a secret from a response type. Warning: Types do not freeze objects; untyped code can still mutate config. Pair DeepReadonly with a recursive Object.freeze (Section 6.3.11) when runtime immutability matters.

    7.7.6 Template Literal Types

    A template literal type puts the backtick syntax of template strings (Section 6.12.2) in a type position: with type Version = "v1" | "v2", ` /api/${Version} is "/api/v1" | "/api/v2", and several unions in one template multiply into every combination. Four helpers built into the compiler, Uppercase, Lowercase, Capitalize and Uncapitalize, transform the strings. Inside a conditional type, a template with infer` parses a string literal, so a router can read parameter names straight out of a path:

    type ParamNames<Path extends string> =

    Path extends `${string}:${infer Name}/${infer Rest}` ? Name | ParamNames<Rest>

    : Path extends `${string}:${infer Name}` ? Name

    : never;

    type RouteParams<Path extends string> = Record<ParamNames<Path>, string>;

    function buildUrl<P extends string>(path: P, params: RouteParams<P>): string {

    return path.replace(/:(\w+)/g, (_, name: ParamNames<P>) => encodeURIComponent(params[name]));

    }

    console.log(buildUrl("/users/:userId/posts/:postId", { userId: "42", postId: "7" }));

    buildUrl("/users/:userId/posts/:postId", { userId: "42" });

    Listing 7.28: Route parameters parsed from the path

    $ npx tsc --noEmit

    routes.ts(11,42): error TS2741: Property 'postId' is missing in type '{ userId: string; }' but

    required in type 'RouteParams<"/users/:userId/posts/:postId">'.

    $ node routes.ts

    /users/42/posts/7

    Output of Listing 7.28 For "/users/:userId/posts/:postId" the first branch matches Name = "userId" and recurses on "posts/:postId", whose second branch yields "postId". The Express type definitions (@types/express-serve-static-core) type req.params the same way. Tip: Combine template literals with key remapping (Section 7.7.5): ` { [K in keyof T as set${Capitalize<K & string>}]: (value: T[K]) => void } generates setName, setEmail and so on. Unions multiply fast: six ${Digit} placeholders of ten digits each (a million strings) fail with error TS2590: Expression produces a union type that is too complex to represent`.

    7.7.7 Utility Types

    TypeScript's standard library (lib.es5.d.ts) defines utility types with the operators of this section: Partial is a mapped type, Exclude a conditional type, NoInfer a compiler intrinsic. Learn them before writing your own: Utility Result Since Partial<T>, Required<T> All keys optional, required 2.1, 2.8 Readonly<T> Top-level keys readonly 2.1 Record<K, V> Keys K, values V 2.1 Pick<T, K>, Omit<T, K> Keep keys K, drop keys K 2.1, 3.5 Exclude<U, M> Union minus members of M 2.8 NonNullable<T> Drops null, undefined 2.8 ReturnType<F> A function's return type 2.8 Parameters<F> A function's parameter tuple 3.1 Awaited<T> Unwrapped promise result 4.5 NoInfer<T> Blocks inference here 5.4 Table 7.9: The most used utility types and the TypeScript version that added them

    interface User { id: number; name: string; email: string; role: "admin" | "member" }

    type NewUser = Omit<User, "id">; // a signup form's payload

    type UserPatch = Partial<Pick<User, "name" | "email">>; // { name?: string; email?: string }

    const roleLabels: Record<User["role"], string> = { admin: "Administrator", member: "Member" };

    declare function updateUser(id: number, patch: UserPatch): Promise<User>;

    type Updated = Awaited<ReturnType<typeof updateUser>>; // User

    function choose<T extends string>(sizes: T[], fallback: NoInfer<T>): T {

    return sizes[0] ?? fallback;

    }

    const draft: NewUser = { id: 7, name: "Ada", email: "ada@example.com", role: "admin" };

    updateUser(1, { role: "admin" });

    choose(["sm", "md", "lg"], "xl");

    Listing 7.29: Deriving request types from one model

    users.ts(10,26): error TS2353: Object literal may only specify known properties, and 'id' does

    not exist in type 'NewUser'.

    users.ts(11,17): error TS2353: Object literal may only specify known properties, and 'role'

    does not exist in type 'Partial<Pick<User, "email" | "name">>'.

    users.ts(12,28): error TS2345: Argument of type '"xl"' is not assignable to parameter of type

    '"lg" | "md" | "sm"'.

    Output of Listing 7.29 Without NoInfer, TypeScript would also infer T from "xl" and accept it. Record<User["role"], string> forces a label for every role, so adding "guest" to User breaks the build until roleLabels is updated. Tip: Omit<T, K> accepts any string as K (it is Pick<T, Exclude<keyof T, K>> with K extends keyof any), so a typo such as Omit<User, "ID"> silently removes nothing. The type-fest library's Except<T, K> is a stricter version (Section 7.13).

    7.8 Classes

    TypeScript classes are JavaScript classes (Section 6.7) with the same fields, getters, static blocks, #private members and extends chains. What TypeScript adds is a set of modifiers that the checker enforces and the emitter mostly erases. Under strict (the default since TypeScript 6.0) every field must also be initialized in its declaration or the constructor; write name!: string when a framework assigns it later. Addition Example Checked by Emitted JavaScript public / protected / private private balance: number Checker only Ordinary property readonly readonly id: string Checker only Ordinary property Parameter property constructor(private r: number) Checker Field plus this.r = r abstract abstract area(): number Checker Member removed implements class A implements B Checker Removed override override print() Checker Removed Table 7.10: What TypeScript adds to a JavaScript class Prefix a constructor parameter with public, private, protected or readonly and TypeScript declares the field and assigns it for you, private members are visible only inside the declaring class, protected members also inside subclasses. The JavaScript # prefix is different: it is enforced by the engine at run time.

    class Account {

    #pin = "0000"; // JavaScript private field

    constructor(

    public readonly owner: string, // parameter properties

    private balance: number,

    protected currency = "EUR",

    ) {}

    deposit(amount: number) { return (this.balance += amount); }

    }

    const acct = new Account("Ada", 100);

    console.log(acct.deposit(50), acct["balance"]); // bracket escape hatch compiles

    acct.owner = "Eve";

    acct.balance = 1e6;

    acct.#pin;

    Listing 7.30: Parameter properties, TypeScript private and JavaScript #private

    account.ts(14,6): error TS2540: Cannot assign to 'owner' because it is a read-only property.

    account.ts(15,6): error TS2341: Property 'balance' is private and only accessible within class

    'Account'.

    account.ts(16,6): error TS18013: Property '#pin' is not accessible outside class 'Account'

    because it has a private identifier.

    Output of Listing 7.30 The emitted constructor is plain JavaScript: field declarations owner; balance; currency; and this.owner = owner assignments. Without the three error lines it logs 150 150. Keep the last line and Node.js refuses the file: SyntaxError: Private field '#pin' must be declared in an enclosing class. private balance #balance Enforced Compile time only Run time, by the engine Bracket access obj["balance"] Allowed Impossible Seen by JSON.stringify, Object.keys Yes No Same name redeclared in a subclass Error TS2415 Allowed, separate slot Table 7.11: TypeScript private versus JavaScript #private Prefer #private for real encapsulation in libraries. TypeScript's private remains handy for parameter properties and for tests that must peek inside. Remember from Section 7.1.4 that parameter properties generate code, so erasableSyntaxOnly and Node.js type stripping reject them.An abstract class cannot be instantiated and may declare abstract members without a body; every concrete subclass must implement them. JavaScript has no such keyword (Section 6.7.5 fakes it with a new.target check), so TypeScript deletes abstract members on emit. implements checks that a class satisfies one or more interfaces, and override, made mandatory by noImplicitOverride, catches methods that stop overriding anything after a base class is renamed.

    interface Printable {

    print(prefix: string): string;

    }

    abstract class Shape implements Printable {

    abstract area(): number; // no body: subclasses must implement

    print(prefix: string) { return `${prefix}${this.area().toFixed(2)}`; }

    }

    class Circle extends Shape {

    constructor(private r: number) { super(); }

    area() { return Math.PI * this.r ** 2; }

    override print(prefix: string) { return `Circle ${super.print(prefix)}`; }

    }

    console.log(new Circle(1).print("area="));

    class Square extends Shape {}

    new Shape();

    class Label implements Printable {

    print(prefix) { return prefix; }

    }

    class Ring extends Circle {

    area() { return 0; }

    }

    Listing 7.31: An abstract base class, an interface and override

    shapes.ts(18,7): error TS2515: Non-abstract class 'Square' does not implement inherited

    abstract member area from class 'Shape'.

    shapes.ts(19,1): error TS2511: Cannot create an instance of an abstract class.

    shapes.ts(21,9): error TS7006: Parameter 'prefix' implicitly has an 'any' type.

    shapes.ts(24,3): error TS4114: This member must have an 'override' modifier because it

    overrides a member in the base class 'Circle'.

    Output of Listing 7.31 Compiled with --noImplicitOverride. The valid part logs Circle area=3.14. The Label error is the classic surprise: implements is only a check and does not give method parameters their types, so you still annotate them. Tip: Choose an abstract class when subclasses share code (here print), and an interface when unrelated classes only share a shape. Interfaces vanish entirely at run time, so instanceof Printable is impossible; an abstract class remains a real constructor you can test with instanceof.

    7.9 Modules

    You write modules with the import and export syntax of Section 6.2.5, but TypeScript does not load them: Node.js, Deno, Bun or a bundler such as Vite does. Two options tell the compiler how that host behaves. module says which format each output file must use, and moduleResolution says how the host turns a specifier such as "./cart.js" into a file. TypeScript never rewrites your specifiers during emit, so it must reject imports that the real host would fail to find. Code runs in module moduleResolution Write relative imports as Vite, webpack, esbuild, Bun esnext or preserve bundler ./cart Node.js, compiled by tsc nodenext nodenext (implied) ./cart.js Node.js type stripping nodenext + rewriteRelativeImportExtensions nodenext ./cart.ts Table 7.12: Choosing module settings The handbook is blunt about Node.js: node16, node18, node20 and nodenext are the only correct module values for code it runs, even if every file is ESM. With a bundler, tsc usually only type-checks (noEmit); adding allowImportingTsExtensions then permits ./cart.ts, which otherwise fails with TS5097. TypeScript 6.0 changed the default module to esnext and deprecated the old node10 resolution, which 7.0 removed.Under nodenext, the extension decides a file's format exactly as Node.js does: .mts is always ESM and emits .mjs, .cts is always CommonJS and emits .cjs, and .ts follows the "type" field of the nearest package.json. ESM imports need full extensions, and you write the extension of the output file (.js), because the specifier is copied verbatim. The "imports" field adds subpath imports, private aliases starting with #. Node.js 25.4 and 24.14 accept the short #/ prefix, and TypeScript 6.0 resolves it under nodenext and bundler, mapping dist paths back to the sources in src.

    { "name": "shop", "type": "module", "imports": { "#/*": "./dist/*" } }

    { "compilerOptions": { "module": "nodenext", "rootDir": "src", "outDir": "dist",

    "verbatimModuleSyntax": true, "types": ["node"] } }

    Listing 7.32: package.json and tsconfig.json for an ESM Node.js project Introduced in TypeScript 5.0, verbatimModuleSyntax makes emit predictable: an import disappears only if it is marked type, everything else stays exactly as written. Single-file tools such as esbuild, SWC and Node.js type stripping cannot tell whether Money is a type, so the flag makes you say so. It also bans ESM syntax in CommonJS files, where you write import x = require() and export = instead.

    // src/lib/legacy.cts (CommonJS)

    function tax(amount: number) { return amount * 0.2; }

    export = tax;

    // src/main.ts (ESM because of "type": "module")

    import { format, Money } from "#/lib/money.js"; // subpath import

    import tax from "./lib/legacy.cjs"; // default import = module.exports

    import { format as f2 } from "./lib/money"; // no extension

    const price: Money = { amount: 10, currency: "EUR" };

    console.log(format(price), tax(price.amount));

    Listing 7.33: src/main.ts and src/lib/legacy.cts under nodenext

    src/main.ts(1,18): error TS1484: 'Money' is a type and must be imported using a type-only

    import when 'verbatimModuleSyntax' is enabled.

    src/main.ts(3,30): error TS2835: Relative import paths need explicit file extensions in

    ECMAScript imports when '--moduleResolution' is 'node16' or 'nodenext'. Did you mean

    './lib/money.js'?

    Output of Listing 7.33 After writing type Money and deleting the extensionless import, tsc emits legacy.cjs with module.exports = tax;, and node dist/main.js prints 10.00 EUR 2. The emitted main.js keeps import { format } from "#/lib/money.js" byte for byte. Writing export const in the .cts file instead fails with TS1287, and an import { readFileSync } from "node:fs" there fails with TS1286. Direction Written as Result in Node.js ESM imports CJS import tax from "./legacy.cjs" Default import is module.exports CJS requires ESM import m = require("./money.js") in .cts Namespace object (Node.js 22.12+) CJS exports one value export = tax in .cts module.exports = tax Table 7.13: ESM and CommonJS interop under nodenext Named imports from CommonJS work only for exports Node.js can detect statically, so the default import is the safe choice. This behavior is esModuleInterop, which TypeScript 6.0 made permanent (it can no longer be false). Old code that wrote import * as express from "express" and called express() should switch to a default import. Tip: For a library, compile with nodenext even if a bundler consumes it: the handbook notes that code compatible with Node.js almost always works in bundlers too, while bundler resolution lets bundler-only imports slip into your output. Check the published package with npx @arethetypeswrong/cli --pack . (Section 7.11).

    7.10 Reference Topics

    These handbook reference topics appear in existing code more often than in new code. Four generate JavaScript (enums, decorators, namespaces, JSX) and deserve a closer look below; the rest fit in a table. Topic What TypeScript adds Example Symbols unique symbol type for const or static readonly const ID: unique symbol = Symbol() Iterators Iterable<T> and Generator<T, TReturn, TNext> function* ids(): Generator<number> Mixins A constructor type for class factories (Section 6.7.4) type Ctor = new (...a: any[]) => {} Triple-slash directives File-top comments naming dependencies /// <reference types="node" /> Type compatibility Structural: same shape, same type A Dog class fits a Pet interface Type inference Best common type and contextual typing [0, 1, null] is (number | null)[] Table 7.14: Reference topics at a glance Use /// <reference types> only in hand-written .d.ts files; in .ts files the types option of tsconfig.json does the job (Section 7.12). Structural compatibility has one nominal exception: classes with private or protected members are compatible only if those members come from the same declaration.An enum creates a runtime object. Numeric enums get a reverse mapping from value to name; string enums do not. The handbook itself says you may not need an enum when an object with as const suffices, and erasableSyntaxOnly (Section 7.1.4) rejects enums outright.

    enum Level { Low = 1, High } // numeric: reverse mapping

    enum Dir { Up = "UP", Down = "DOWN" } // string: no reverse mapping

    const Status = { Active: "active", Archived: "archived" } as const;

    type Status = (typeof Status)[keyof typeof Status]; // "active" | "archived"

    function move(d: Dir) { return d; }

    function setStatus(s: Status) { return s; }

    console.log(Level.High, Level[2], move(Dir.Up), setStatus("archived"));

    move("UP");

    const lvl: Level = 7;

    Listing 7.34: A numeric enum, a string enum and an as const object

    enums.ts(12,6): error TS2345: Argument of type '"UP"' is not assignable to parameter of type

    'Dir'.

    enums.ts(13,7): error TS2322: Type '7' is not assignable to type 'Level'.

    Output of Listing 7.34 The valid lines log 2 High UP archived, and Level compiles to Level[Level["Low"] = 1] = "Low" inside an IIFE. A string enum is nominal: even the exact string "UP" is refused, so callers must import Dir. The union type accepts a plain "archived" and costs nothing at run time. const enum inlines values but breaks under isolatedModules when it comes from a declaration file, so avoid it in libraries.TypeScript 5.0 implemented the standard ECMAScript decorators described in Section 6.7.11, with context types such as ClassMethodDecoratorContext that make a decorator generic over the method it wraps. The older experimentalDecorators flag remains for code written against the earlier proposal.

    function logged<This, Args extends unknown[], R>(

    method: (this: This, ...args: Args) => R,

    context: ClassMethodDecoratorContext<This, (this: This, ...args: Args) => R>,

    ) {

    return function (this: This, ...args: Args): R {

    console.log(`${String(context.name)}(${args.join(", ")})`);

    return method.call(this, ...args);

    };

    }

    class Cart {

    @logged add(price: number, qty: number) { return price * qty; }

    }

    console.log(new Cart().add(4, 3));

    Listing 7.35: A type-safe standard method decorator

    add(4, 3)

    12

    Output of Listing 7.35 Put @logged on a field instead and tsc reports TS1240, "Unable to resolve signature of property decorator when called as an expression". The emitted file contains an __esDecorate helper, because no runtime supports the syntax yet. Standard decorators cannot decorate parameters and do not work with emitDecoratorMetadata, so the NestJS starter project still sets experimentalDecorators and emitDecoratorMetadata. Lit supports both styles.When two declarations share a name, TypeScript merges them. Interfaces merge member by member, which is how you add properties to library types with declare global or declare module "pkg". A namespace can merge into a function, class or enum to attach static members; classes never merge with classes.

    export {}; // make this file a module

    interface Box { width: number }

    interface Box { height: number } // merged: { width; height }

    const box: Box = { width: 2, height: 3 };

    declare global {

    interface Window { appVersion: string } // augment a lib.dom.d.ts type

    }

    function money(n: number) { return `$${n.toFixed(2)}`; }

    namespace money { // function + namespace merge

    export const currency = "USD";

    }

    console.log(box.width * box.height, money(4), money.currency);

    Listing 7.36: Interface merging, global augmentation and a function-namespace merge 6 $4.00 USD Output of Listing 7.36 Two classes named Box would fail with TS2300, "Duplicate identifier". The namespace compiles to an IIFE that assigns money.currency, which is why erasableSyntaxOnly rejects it with TS1294. Namespaces predate ES modules; in new code use modules (Section 7.9) and keep declare namespace for describing globals in .d.ts files (Section 7.11). TypeScript 6.0 deprecated the old spelling module Foo {}.Files ending in .tsx may contain JSX, which TypeScript type-checks against JSX.IntrinsicElements from the library's types (install @types/react for React). The jsx option picks the output: preserve leaves JSX in a .jsx file for a bundler, react emits React.createElement calls, and react-jsx emits the automatic runtime, so <span className="badge">{n}</span> becomes _jsx("span", { className: "badge", children: n }) with import { jsx as _jsx } from "react/jsx-runtime". Set jsxImportSource to preact to import preact/jsx-runtime instead. Tip: In interviews, "enum or union?" usually means "do you know enums emit code?". Answer with the reverse mapping, erasableSyntaxOnly and the as const alternative.

    7.11 Declaration Files

    A declaration file (.d.ts) holds only types, signatures without bodies, describing JavaScript the compiler cannot see, from lib.dom.d.ts (Chapter 8) to a plain-JavaScript npm package.Beside slugify.js, slugify.d.ts makes slugify(42) in a .ts file fail with error TS2345.

    export interface SlugifyOptions {

    /** Character placed between words. Default "-". */

    separator?: string;

    }

    export declare function slugify(text: string, options?: SlugifyOptions): string;

    Listing 7.37: slugify.d.ts: types for a JavaScript module For packages that ship no types, install community ones such as npm install -D @types/lodash. DefinitelyTyped (https://github.com/DefinitelyTyped/DefinitelyTyped) (51k stars) publishes them to the @types scope. Imported packages find their @types automatically, but global ones such as @types/node must be listed in "types", whose default became [] in TypeScript 6.0. New declarations start from the handbook's seven templates, or from npx dts-gen --dt --name <package> --template module: Library shape Template Key syntax ES module module.d.ts export function CommonJS class or function module-class.d.ts, module-function.d.ts export = Globals from a script or module global.d.ts, global-modifying-module.d.ts declare namespace, declare global Plugin for another library module-plugin.d.ts, global-plugin.d.ts Module augmentation Table 7.15: Declaration file templates from the TypeScript handbook To publish your own library's types, set "declaration": true, point "types" in package.json at the generated file and put the "types" condition first in "exports". npx @arethetypeswrong/cli --pack catches ESM/CommonJS type mismatches before release.Keep .js files and still get checking: add // @ts-check (or "checkJs": true) and write types in JSDoc; @import (TypeScript 5.5) pulls types from other files.

    // @ts-check

    /** @typedef {{ name: string, price: number, qty: number }} CartItem */

    /** @param {CartItem[]} items @param {number} [taxRate] */

    export function total(items, taxRate = 0.08) {

    const sum = items.reduce((acc, item) => acc + item.price * item.quantity, 0);

    return Math.round(sum * (1 + taxRate) * 100) / 100;

    }

    total([{ name: "Pen", price: 2, qty: 3 }], "8%");

    Listing 7.38: cart.js: JSDoc types checked by tsc

    src/cart.js(6,67): error TS2339: Property 'quantity' does not exist on type 'CartItem'.

    src/cart.js(9,44): error TS2345: Argument of type 'string' is not assignable to parameter of

    type 'number'.

    Output of Listing 7.38 tsc --allowJs --declaration --emitDeclarationOnly turns the comments into export declare function total(items: CartItem[], taxRate?: number): number;. The Svelte compiler has been written this way since Svelte 4.

    7.12 Project Configuration

    tsconfig.json marks a project root: compilerOptions sets checking and output, include lists the source folders, and extends inherits a shared base (an array of bases since TypeScript 5.0). npx tsc --init writes a short starter file; npx tsc --showConfig prints the final merged settings, including implied ones.strict and target can stay at their TypeScript 6.0 defaults (Section 7.2). The app column follows the create-vite templates; the library column emits JavaScript and .d.ts files for npm. Option Vite app Library module / moduleResolution esnext / bundler nodenext (implied) Output noEmit: true (Vite emits) outDir: "dist", rootDir: "src" .d.ts files Not needed declaration, declarationMap, isolatedDeclarations types ["vite/client"] ["node"] if used Other flags verbatimModuleSyntax, erasableSyntaxOnly, skipLibCheck Same, plus noUncheckedIndexedAccess Table 7.16: Recommended tsconfig.json options (TypeScript 7.0) isolatedDeclarations requires explicit types on exports, so other tools can write .d.ts files one file at a time. export function first(items: string[]) { return items[0]; } fails with error TS9013: Expression type can't be inferred with --isolatedDeclarations; add : string | undefined and tsc emits the declaration.tsc builds the nearest tsconfig.json; -p <path> picks another, --noEmit only checks, -w rechecks on every save and -b builds project references. Passing file names while a tsconfig.json exists is error TS5112 since 6.0 (add --ignoreConfig). TypeScript 7.0 adds --checkers and --builders to tune its threads (Section 7.1.2).Split a large repository into projects with "composite": true, then list dependencies in "references". tsc -b builds them in order, writes a .tsbuildinfo file per project and skips projects whose inputs are unchanged:

    {

    "compilerOptions": { "module": "nodenext", "outDir": "dist", "rootDir": "src" },

    "references": [{ "path": "../core" }]

    }

    Listing 7.39: app/tsconfig.json referencing a composite core project

    $ npx tsc -b app --verbose

    Project 'core/tsconfig.json' is up to date because newest input 'core/src/index.ts' is older

    than output 'core/tsconfig.tsbuildinfo'

    Project 'app/tsconfig.json' is up to date because newest input 'app/src/main.ts' is older than

    output 'app/dist/main.js'

    Output of Listing 7.39 Vite 8, esbuild, tsx and Node.js strip types one file at a time and never type-check, which is why isolatedModules matters. Keep tsc in the loop: the Vite template's build script is tsc -b && vite build, and vite-plugin-checker (https://github.com/fi3ework/vite-plugin-checker) runs tsc in a separate process and shows errors in the browser. For libraries, tsdown (https://github.com/rolldown/tsdown) bundles JavaScript and declarations with Rolldown.

    7.13 TypeScript in Practice

    Real projects need typed browser APIs, a gradual path from JavaScript, and tools that check what the compiler cannot.lib.dom.d.ts is generated from the web specifications (Chapter 8). querySelector infers the element type from a tag name; for other selectors you pass it. Listeners get their event type from the event name. Lookups may return null and event.target is only an EventTarget, so narrow with instanceof (Section 7.4):

    const form = document.querySelector("form"); // HTMLFormElement | null

    const qty = document.querySelector<HTMLInputElement>("#qty");

    const rows = document.querySelectorAll("tr"); // NodeListOf<HTMLTableRowElement>

    form.addEventListener("submit", (event) => { // event: SubmitEvent

    event.preventDefault();

    rows.forEach((row) => (row.hidden = row.rowIndex > (qty?.valueAsNumber ?? 0)));

    });

    document.addEventListener("click", ({ target }) => { // target: EventTarget | null

    if (target instanceof HTMLButtonElement) target.disabled = true;

    target.disabled = false;

    });

    Listing 7.40: DOM types inferred from selectors and event names

    dom.ts(4,1): error TS18047: 'form' is possibly 'null'.

    dom.ts(10,3): error TS18047: 'target' is possibly 'null'.

    dom.ts(10,10): error TS2339: Property 'disabled' does not exist on type 'EventTarget'.

    Output of Listing 7.40 querySelector<HTMLInputElement> is an unchecked assertion, like as: if #qty is a <select>, the types lie.Migrate in small, shippable steps; types never change runtime behavior, so the app keeps working. Step Setting or action Result 1. Include JavaScript "allowJs": true, "noEmit": true tsc sees the whole project 2. Check JavaScript // @ts-check per file, then "checkJs": true Errors in .js, fixed with JSDoc (Section 7.11) 3. Rename leaf modules .js to .ts, utilities first Real annotations; // @ts-nocheck parks noisy files 4. Tighten "strict": true with "noImplicitAny": false, then remove it Full strictness, one flag at a time 5. Finish Add @types/*, drop allowJs An all-TypeScript project Table 7.17: A gradual JavaScript-to-TypeScript migration Mark a known problem with // @ts-expect-error rather than // @ts-ignore: it fails once the line is fixed, so stale suppressions do not pile up.JSON from fetch or a form is really unknown. Zod (https://github.com/colinhacks/zod) 4.6 (introduced in Section 6.26) validates such data and infers the static type from the schema, so you declare each shape once. type-fest (https://github.com/sindresorhus/type-fest) 5.9 adds ready-made utility types such as SetRequired, PartialDeep, Simplify and Jsonify, all types-only:

    import * as z from "zod";

    import type { SetRequired } from "type-fest";

    const Order = z.object({ email: z.email(), qty: z.int().positive(), note: z.string().optional() });

    type Order = z.infer<typeof Order>; // { email: string; qty: number; note?: string }

    export type GiftOrder = SetRequired<Order, "note">; // note: string

    const result = Order.safeParse(JSON.parse('{"email":"ada@example","qty":0}'));

    if (result.success) console.log(result.data.qty); // result.data: Order

    else console.log(z.prettifyError(result.error));

    Listing 7.41: One Zod schema gives a validator and a type

    ✖ Invalid email address

    → at email

    ✖ Too small: expected number to be >0

    → at qty

    Output of Listing 7.41 typescript-eslint (https://github.com/typescript-eslint/typescript-eslint) plugs TypeScript's parser and type checker into ESLint (Section 1.9); recommendedTypeChecked enables rules that need type information, such as unhandled promises and unsafe any:

    import js from "@eslint/js";

    import { defineConfig } from "eslint/config";

    import tseslint from "typescript-eslint";

    export default defineConfig({

    files: ["**/*.ts"],

    extends: [js.configs.recommended, tseslint.configs.recommendedTypeChecked],

    languageOptions: { parserOptions: { projectService: true } },

    });

    Listing 7.42: eslint.config.js with type-aware rules A call save(order); to an async function, without await, passes tsc but not the linter:

    5:3 error Promises must be awaited, end with a call to .catch, end with a call to .then

    with a rejection handler or be explicitly marked as ignored with the `void` operator

    @typescript-eslint/no-floating-promises

    Output of Listing 7.42 Warning: typescript-eslint 8.70 imports the compiler API and declares typescript <6.1.0 as a peer dependency, so plain npm install fails beside TypeScript 7.0. Use the npm aliases from Section 7.1.2 until TypeScript 7.1 restores the API. Library authors also test the types themselves. expect-type (https://github.com/mmkal/expect-type) turns type assertions into compile errors; Vitest's expectTypeOf is built on it and checks *.test-d.ts files with vitest --typecheck:

    import { expectTypeOf } from "expect-type";

    import * as z from "zod";

    const Order = z.object({ email: z.email(), qty: z.int() });

    expectTypeOf<z.infer<typeof Order>>().toEqualTypeOf<{ email: string; qty: number }>();

    expectTypeOf(Order.parse).returns.toEqualTypeOf<{ email: string; qty: string }>();

    // @ts-expect-error: qty must be a number

    Order.parse satisfies (data: unknown) => { qty: string };

    Listing 7.43: Type tests that run under tsc

    types.test.ts(6,49): error TS2344: Type '{ email: string; qty: string; }' does not satisfy the

    constraint '{ email: string; qty: "Expected: string, Actual: number"; }'.

    Output of Listing 7.43

    7.14 What's New

    When you follow an older tutorial or upgrade a project, check which release introduced a feature: Version Released Highlights 5.0 Mar 2023 Standard decorators, const type parameters, bundler mode 5.1 Jun 2023 Unrelated getter and setter types 5.2 Aug 2023 using declarations, decorator metadata 5.3 Nov 2023 Import attributes, switch (true) narrowing 5.4 Mar 2024 NoInfer<T>, narrowing preserved in closures 5.5 Jun 2024 Inferred type predicates, isolatedDeclarations 5.6 Sep 2024 Errors on always-truthy checks, iterator helpers 5.7 Nov 2024 Never-initialized variable checks 5.8 Feb 2025 erasableSyntaxOnly, require() of ESM in nodenext 5.9 Aug 2025 import defer, minimal tsc --init 6.0 Mar 2026 Last JavaScript-based release; new defaults 7.0 Jul 2026 Native Go compiler (Section 7.1.2) Table 7.18: TypeScript releases from 5.0 to 7.0 (source: official release announcements) Three of these features in one file, checked by tsc 7.0 and run by Node.js 25:

    function openLog(name: string) {

    console.log(`open ${name}`);

    return { [Symbol.dispose]: () => console.log(`close ${name}`) };

    }

    const pick = <T extends string>(options: T[], fallback: NoInfer<T>): T => options[0] ?? fallback;

    {

    using log = openLog("audit"); // disposed at block end

    const ids = [3, undefined, 7].filter((id) => id !== undefined); // number[]

    console.log(ids.map((id) => id * 2), pick(["sm", "lg"], "sm"));

    pick(["sm", "lg"], "xl");

    }

    Listing 7.44: using (5.2), NoInfer (5.4) and inferred type predicates (5.5)

    $ npx tsc --noEmit

    whatsnew.ts(10,22): error TS2345: Argument of type '"xl"' is not assignable to parameter of

    type '"lg" | "sm"'.

    $ node whatsnew.ts

    open audit

    [ 6, 14 ] sm

    close audit

    Output of Listing 7.44 Without NoInfer, T widens to include "xl" and the typo passes. TypeScript 5.4 also reports 'id' is possibly 'undefined': before 5.5 you had to write (id): id is number => ... yourself.

    7.15 Alternatives and Companions

    Three neighbors of TypeScript remain worth knowing: a competitor, a way to use TypeScript without its syntax, and a language that borrows its syntax for WebAssembly. Option Source files Checked by License Best for TypeScript 7.0 .ts tsc Apache-2.0 Almost every project Flow 0.331 .js with // @flow flow (Rust) MIT Meta-style React codebases JSDoc-only Plain .js tsc with checkJs Apache-2.0 Libraries without a build step AssemblyScript 0.28 .ts (own types) asc Apache-2.0 Hot numeric code as WebAssembly Table 7.19: TypeScript and its alternatives (September 2026) Meta's Flow (https://github.com/facebook/flow) (22k stars) checks "millions of files" inside Meta. Its syntax is close to TypeScript's, but objects are exact by default and it adds component, renders and match syntax. Install flow-bin, run flow init, and strip annotations with @babel/preset-flow or flow-remove-types. Since 2021 Flow has put Meta's own codebase first, so outside Meta choose it only to maintain existing Flow code.The same tsc checks types written in comments (Section 7.11), so files run unchanged in browsers and Node.js and no source maps are needed. You pay with verbose syntax. Node.js type stripping (Section 7.1.4) now weakens the no-build argument for .ts files. Did you know? The TC39 Type Annotations proposal (https://github.com/tc39/proposal-type-annotations) would let engines ignore TypeScript-style annotations as comments. It reached Stage 1 in March 2022 and has not advanced since. AssemblyScript (https://github.com/AssemblyScript/assemblyscript) (18k stars) compiles a variant of TypeScript to WebAssembly through Binaryen. Types are machine types (i32, i64, f64), and dynamic features such as any and union types are not supported:

    export function fib(n: i32): i64 {

    let a: i64 = 0, b: i64 = 1;

    for (let i = 0; i < n; i++) {

    const t = a + b;

    a = b;

    b = t;

    }

    return a;

    }

    Listing 7.45: assembly/index.ts compiled with npx asc --optimize --bindings esm

    $ node -e "import('./build/fib.js').then(m => console.log(m.fib(90), m.fib(3.9)))"

    2880067194370816120n 2n

    Output of Listing 7.45 The optimized fib.wasm is 90 bytes; the bindings map i64 to bigint and truncate 3.9 to 3. Use AssemblyScript for computation, not DOM code, which still needs JavaScript.