================================================================================ TYPESCRIPT-CHEATSHEET.TXT ================================================================================ NAME typescript-cheatsheet.txt -- offline TypeScript reference manual SYNOPSIS grep -A 40 "^SECTION_NAME" typescript-cheatsheet.txt less typescript-cheatsheet.txt grep -n "SECTION_NAME" typescript-cheatsheet.txt DESCRIPTION This file is a single flat plain-text reference manual covering the TypeScript type system: the compiler and tsconfig, basic and advanced types, interfaces, generics, narrowing, conditional and mapped types, utility types, declaration files, and TypeScript as applied to React, Node, and Express. It covers ONLY what TypeScript adds. Runtime behavior, standard library methods, algorithms, and everything else about the language live in javascript-cheatsheet.txt. Every example there is valid TypeScript. It is written to be grepped, not rendered. Every major topic has a large banner header made of '=' characters. Every subsection inside a topic has a smaller banner made of '-' characters. There is no markdown, no bullets unless natural, and no code fences. Code is always indented four spaces. Standard subsection names, greppable across the whole file: DESCRIPTION CREATE COMMON METHODS EXAMPLES COMMON MISTAKES INTERVIEW NOTES SUGGESTED ALIAS Add something like this to your shell rc file: tsts() { if [ -z "$1" ]; then less ~/typescript-cheatsheet.txt else grep -n -i --color=always "$*" ~/typescript-cheatsheet.txt | less -R fi } Usage: tsts GENERICS tsts CREATE tsts UTILITY TYPES tsts NARROWING tsts TSCONFIG Since every section header is a unique all-caps banner line, grep -n will point you straight at the line number, and you can jump there directly with: less + ~/typescript-cheatsheet.txt TABLE OF CONTENTS MENTAL MODEL TSC CLI TSCONFIG BASIC TYPES ANY UNKNOWN NEVER VOID ARRAYS AND TUPLES OBJECT TYPES INTERFACES TYPE ALIASES INTERFACE VS TYPE UNIONS AND INTERSECTIONS LITERAL TYPES ENUMS NARROWING TYPE GUARDS DISCRIMINATED UNIONS FUNCTIONS CLASSES GENERICS KEYOF TYPEOF INDEXED ACCESS CONDITIONAL TYPES MAPPED TYPES TEMPLATE LITERAL TYPES UTILITY TYPES TYPE ASSERTIONS SATISFIES MODULES AND DECLARATION FILES ASYNC TYPES ERROR HANDLING REACT NODE AND EXPRESS RUNTIME VALIDATION TESTING MIGRATING FROM JAVASCRIPT COMMON COMPILER ERRORS BEST PRACTICES ================================================================================ MENTAL MODEL ================================================================================ DESCRIPTION Coming from C#, three differences matter more than any syntax detail. ------------------------------------------------------------------------------- TYPES ARE ERASED ------------------------------------------------------------------------------- TypeScript compiles to JavaScript by deleting every type. Nothing about the type system exists at runtime. There is no reflection, no typeof for a type, no generic type available inside a generic function. function f(x: T) { // there is no way to ask what T is here } This is the opposite of C#, where generics are reified and typeof(T) works. Anything you need at runtime must be a runtime value: a string tag, a schema, a constructor reference. ------------------------------------------------------------------------------- STRUCTURAL TYPING ------------------------------------------------------------------------------- C# is nominal: a type matches only if it declares it implements the interface. TypeScript is structural: a type matches if its shape fits. interface Point { x: number; y: number; } const p = { x: 1, y: 2, z: 3 }; const q: Point = p; // fine, it has x and y You never write "implements" to satisfy an interface (though the keyword exists on classes as an assertion). Shape is everything. ------------------------------------------------------------------------------- THE TYPE SYSTEM IS NOT SOUND ------------------------------------------------------------------------------- TypeScript deliberately allows some unsafe things for pragmatism: any, type assertions, array index access returning T instead of T | undefined by default, and method parameter bivariance. It is a productivity tool that catches most mistakes, not a proof system. Turn on strict and noUncheckedIndexedAccess to close the largest gaps. ================================================================================ TSC CLI ================================================================================ DESCRIPTION tsc is the TypeScript compiler. Use it to typecheck (often with --noEmit) and to emit JS when you are not using a bundler. It does not bundle. ------------------------------------------------------------------------------- CREATE ------------------------------------------------------------------------------- npm install -D typescript npx tsc --version ------------------------------------------------------------------------------- INITIALIZE ------------------------------------------------------------------------------- npx tsc --init npx tsc --init --strict ------------------------------------------------------------------------------- COMPILE ------------------------------------------------------------------------------- npx tsc // uses tsconfig.json npx tsc --watch // recompile on change npx tsc --noEmit // type check only, no output npx tsc file.ts // single file, ignores tsconfig ------------------------------------------------------------------------------- USEFUL FLAGS ------------------------------------------------------------------------------- --noEmit check only, for CI and pre-commit --strict enable all strict checks --target es2022 --module nodenext --outDir dist --declaration emit .d.ts files --sourceMap --incremental cache for faster rebuilds --listFiles which files were included --traceResolution debug module resolution --showConfig print the effective config ------------------------------------------------------------------------------- RUNNING TYPESCRIPT DIRECTLY ------------------------------------------------------------------------------- node --experimental-strip-types main.ts // Node 22+ npx tsx main.ts // tsx, no config needed npx ts-node main.ts // older, slower ------------------------------------------------------------------------------- TYPICAL PACKAGE.JSON ------------------------------------------------------------------------------- "scripts": { "build": "tsc", "dev": "tsx watch src/main.ts", "typecheck": "tsc --noEmit", "test": "vitest run" } ------------------------------------------------------------------------------- NOTES ------------------------------------------------------------------------------- tsc does not bundle. For applications, a bundler (Vite, esbuild, swc) handles transpilation and tsc --noEmit handles type checking. They are separate jobs and running both is normal. ================================================================================ TSCONFIG ================================================================================ DESCRIPTION tsconfig.json is the compiler's configuration. strict is the default you want on a new project. module and moduleResolution must agree. ------------------------------------------------------------------------------- CREATE ------------------------------------------------------------------------------- { "compilerOptions": { "target": "ES2022", "module": "NodeNext", "moduleResolution": "NodeNext", "lib": ["ES2022"], "outDir": "dist", "rootDir": "src", "strict": true, "noUncheckedIndexedAccess": true, "noImplicitOverride": true, "exactOptionalPropertyTypes": true, "noFallthroughCasesInSwitch": true, "noUnusedLocals": true, "noUnusedParameters": true, "esModuleInterop": true, "forceConsistentCasingInFileNames": true, "skipLibCheck": true, "resolveJsonModule": true, "isolatedModules": true, "declaration": true, "sourceMap": true }, "include": ["src/**/*"], "exclude": ["node_modules", "dist"] } ------------------------------------------------------------------------------- WHAT STRICT TURNS ON ------------------------------------------------------------------------------- noImplicitAny untyped parameters become an error strictNullChecks null and undefined are separate types strictFunctionTypes contravariant parameter checking strictBindCallApply type-checks bind/call/apply strictPropertyInitialization class fields must be assigned noImplicitThis this must have a known type useUnknownInCatchVariables catch (e) is unknown, not any alwaysStrict emits "use strict" strictNullChecks is the single most valuable one. Without it, every type silently includes null and undefined and the compiler catches almost nothing useful. ------------------------------------------------------------------------------- OPTIONS WORTH ENABLING BEYOND STRICT ------------------------------------------------------------------------------- noUncheckedIndexedAccess arr[0] becomes T | undefined, and obj[key] likewise. Annoying at first, correct always. This is the biggest remaining soundness gap that strict does not close. exactOptionalPropertyTypes { a?: string } no longer accepts { a: undefined }. Distinguishes "absent" from "explicitly undefined". noImplicitOverride requires the override keyword when overriding a base method, matching C# behavior and catching renamed-base-method bugs. ------------------------------------------------------------------------------- PATH ALIASES ------------------------------------------------------------------------------- "baseUrl": ".", "paths": { "@/*": ["src/*"], "@components/*": ["src/components/*"] } import { Button } from "@/components/Button"; tsc resolves these for type checking but does NOT rewrite them in output. The bundler or runtime must be configured to match (Vite resolve.alias, tsconfig-paths for Node). ------------------------------------------------------------------------------- PROJECT REFERENCES ------------------------------------------------------------------------------- For monorepos, split into sub-projects that build independently: { "references": [ { "path": "./packages/shared" }, { "path": "./packages/api" } ] } npx tsc --build Requires "composite": true in each referenced project. ------------------------------------------------------------------------------- COMMON MISTAKES ------------------------------------------------------------------------------- Leaving strict off on a new project. It is far harder to turn on later. Setting skipLibCheck: false and then fighting errors inside third-party .d.ts files you cannot change. skipLibCheck: true is the sane default. Mismatching module and moduleResolution (for example module: ESNext with moduleResolution: Node10), which produces confusing import errors. Forgetting that tsconfig include/exclude affects which files are checked, so a file outside include silently gets no checking at all. ================================================================================ BASIC TYPES ================================================================================ DESCRIPTION The primitive types, inference, const assertions, and the places you actually write annotations. Prefer inference; annotate parameters and public returns. ------------------------------------------------------------------------------- CREATE ------------------------------------------------------------------------------- let s: string = "hello"; let n: number = 42; // one number type, no int/float/decimal let b: boolean = true; let big: bigint = 10n; let sym: symbol = Symbol("id"); let u: undefined = undefined; let nl: null = null; Always lowercase. String, Number, Boolean are the wrapper object types and are almost never what you want. ------------------------------------------------------------------------------- INFERENCE ------------------------------------------------------------------------------- let x = 42; // number const y = 42; // 42, a literal type let name = "Jake"; // string const arr = [1, 2, 3]; // number[] const obj = { a: 1 }; // { a: number } Do not annotate what is already inferred. Annotate function parameters, function return types on public APIs, and empty containers. const items = []; // any[], implicitly const items: string[] = []; // annotate ------------------------------------------------------------------------------- CONST ASSERTIONS ------------------------------------------------------------------------------- const config = { mode: "dark", retries: 3 } as const; // { readonly mode: "dark"; readonly retries: 3 } const roles = ["admin", "user"] as const; // readonly ["admin", "user"] type Role = typeof roles[number]; // "admin" | "user" This is the standard way to derive a union type from a runtime array, keeping one source of truth. ------------------------------------------------------------------------------- TYPE ANNOTATION SITES ------------------------------------------------------------------------------- let x: string; function f(a: number, b: string): boolean { } const g = (a: number): string => ""; class C { field: string; } interface I { prop: number; } const h = (x: T): T => x; // trailing comma needed in .tsx ================================================================================ ANY UNKNOWN NEVER VOID ================================================================================ DESCRIPTION any disables checking. unknown is the safe top type. never is the empty type. void is 'ignore the return'. These four are how TypeScript talks about absence and escape hatches. ------------------------------------------------------------------------------- ANY ------------------------------------------------------------------------------- Disables all checking on that value, and spreads: anything derived from an any is also any. It is an escape hatch, not a type. const data: any = JSON.parse(s); data.whatever.at.all; // no error, may crash Use it only to unblock a migration, and leave a TODO. ------------------------------------------------------------------------------- UNKNOWN ------------------------------------------------------------------------------- The safe version of any. Anything is assignable TO unknown, but unknown is assignable to nothing without narrowing first. const data: unknown = JSON.parse(s); data.name; // error if (typeof data === "object" && data !== null && "name" in data) { // now usable } Use unknown for: JSON.parse results, catch variables, external API responses, and anything you must validate before trusting. ------------------------------------------------------------------------------- NEVER ------------------------------------------------------------------------------- The type with no values. Produced by a function that never returns, by an impossible narrowing, and by an empty union. function fail(msg: string): never { throw new Error(msg); } Its most useful role is exhaustiveness checking: function area(shape: Shape): number { switch (shape.kind) { case "circle": return Math.PI * shape.r ** 2; case "square": return shape.side ** 2; default: { const exhaustive: never = shape; throw new Error(`unhandled: ${exhaustive}`); } } } Add a new member to Shape and this fails to compile until handled. It is the closest TypeScript comes to a sealed hierarchy with exhaustive matching. ------------------------------------------------------------------------------- VOID ------------------------------------------------------------------------------- The return type of a function that returns nothing useful. function log(msg: string): void { console.log(msg); } void as a return type in a callback position means "the return value is ignored", so a function returning something is still assignable: const nums = [1, 2, 3]; nums.forEach((n) => map.set(n, n)); // set returns Map, fine ------------------------------------------------------------------------------- NULL AND UNDEFINED ------------------------------------------------------------------------------- With strictNullChecks, they are distinct types and must be handled: function f(name: string | null) { name.length; // error name?.length; // number | undefined if (name) name.length; // narrowed to string (name ?? "").length; // number } Optional property vs union with undefined: interface A { x?: number; } // may be absent interface B { x: number | undefined; } // must be present ------------------------------------------------------------------------------- COMMON MISTAKES ------------------------------------------------------------------------------- Using any to silence an error rather than understanding it. It usually moves the failure to runtime. Typing a catch variable as any (or leaving it, pre-strict) and calling err.message. With useUnknownInCatchVariables it is unknown, and you must narrow: catch (err) { const msg = err instanceof Error ? err.message : String(err); } Confusing never and void. void means "returns nothing"; never means "does not return at all". ================================================================================ ARRAYS AND TUPLES ================================================================================ DESCRIPTION T[] / Array is a homogeneous list. A tuple is a fixed-length array with per-index types. noUncheckedIndexedAccess makes indexing honest. ------------------------------------------------------------------------------- ARRAYS ------------------------------------------------------------------------------- let a: number[] = [1, 2, 3]; let b: Array = [1, 2, 3]; // equivalent let c: (string | number)[] = [1, "a"]; let d: readonly number[] = [1, 2, 3]; let e: ReadonlyArray = [1, 2, 3]; readonly arrays reject push, pop, splice, sort, and index assignment at compile time. ------------------------------------------------------------------------------- TUPLES ------------------------------------------------------------------------------- let pair: [string, number] = ["a", 1]; let named: [name: string, age: number] = ["Jake", 30]; let optional: [string, number?] = ["a"]; let rest: [string, ...number[]] = ["a", 1, 2, 3]; let frozen: readonly [number, number] = [1, 2]; Tuples are how useState-style returns are typed: function useToggle(): [boolean, () => void] { } ------------------------------------------------------------------------------- NOUNCHECKEDINDEXEDACCESS ------------------------------------------------------------------------------- Without it: const first = arr[0]; // number, even if arr is empty With it: const first = arr[0]; // number | undefined, correct Handle it with a guard, .at(), or a non-null assertion where you have genuinely proven it: const first = arr[0]; if (first === undefined) return; ------------------------------------------------------------------------------- COMMON MISTAKES ------------------------------------------------------------------------------- Annotating an array of objects as any[] instead of defining the element type, losing all checking inside map and filter callbacks. Assuming a tuple keeps its tuple-ness after a method call. map on a tuple returns an array, not a tuple, unless typed with a mapped tuple. Expecting readonly to be deep. readonly T[] only prevents mutating the array, not the objects inside it. ================================================================================ OBJECT TYPES ================================================================================ DESCRIPTION Object types describe shapes: required and optional properties, readonly, index signatures, and excess-property checking on fresh literals. ------------------------------------------------------------------------------- CREATE ------------------------------------------------------------------------------- let user: { name: string; age: number }; function greet(p: { name: string }): string { return p.name; } ------------------------------------------------------------------------------- MODIFIERS ------------------------------------------------------------------------------- interface User { readonly id: number; // cannot be reassigned after creation name: string; age?: number; // optional: number | undefined [key: string]: unknown; // index signature } ------------------------------------------------------------------------------- INDEX SIGNATURES ------------------------------------------------------------------------------- interface StringMap { [key: string]: string; } interface NumberMap { [index: number]: string; } Every declared property must be assignable to the index signature type, which is why mixing them often fails: interface Bad { [key: string]: string; count: number; // error } Prefer Record for the common case, and a Map for dynamic keys. ------------------------------------------------------------------------------- EXCESS PROPERTY CHECKING ------------------------------------------------------------------------------- Object literals assigned directly get an extra check that variables do not: interface Point { x: number; y: number; } const p: Point = { x: 1, y: 2, z: 3 }; // error, z is excess const temp = { x: 1, y: 2, z: 3 }; const q: Point = temp; // fine, structural This is deliberate: a literal with an unexpected key is almost always a typo, while an existing wider object being narrowed is normal. ------------------------------------------------------------------------------- COMMON MISTAKES ------------------------------------------------------------------------------- Being surprised that an object with extra properties is assignable through a variable but not as a literal. See above; it is by design. Using an index signature to avoid defining real keys, which throws away autocomplete and typo detection. ================================================================================ INTERFACES ================================================================================ DESCRIPTION interface names an object shape that can be extended and merged. Use it for public object APIs; use type for unions and computed types. ------------------------------------------------------------------------------- CREATE ------------------------------------------------------------------------------- interface User { id: number; name: string; greet(): string; // method onChange: (value: string) => void; // property holding a function } ------------------------------------------------------------------------------- EXTENDING ------------------------------------------------------------------------------- interface Admin extends User { level: number; } interface Both extends User, Timestamped { } ------------------------------------------------------------------------------- DECLARATION MERGING ------------------------------------------------------------------------------- Two interfaces with the same name in the same scope merge: interface Window { myApp: App; } This is how you augment third-party types (Express Request, the global Window). Type aliases cannot do this. ------------------------------------------------------------------------------- IMPLEMENTING IN A CLASS ------------------------------------------------------------------------------- class UserImpl implements User { constructor(public id: number, public name: string) {} greet() { return `Hi, ${this.name}`; } onChange = (value: string) => {}; } implements is only an assertion that the class satisfies the shape. It does not change the class's type, and it is not required for the class to be assignable to the interface (structural typing). ------------------------------------------------------------------------------- CALL AND CONSTRUCT SIGNATURES ------------------------------------------------------------------------------- interface Formatter { (value: string): string; // callable locale: string; // and has properties } interface UserConstructor { new (name: string): User; // constructible } ================================================================================ TYPE ALIASES ================================================================================ DESCRIPTION type names any type: unions, primitives, tuples, functions, mapped and conditional types. Interfaces can only describe object shapes. ------------------------------------------------------------------------------- CREATE ------------------------------------------------------------------------------- type ID = string | number; type Point = { x: number; y: number }; type Handler = (event: Event) => void; type Pair = [T, T]; type Nullable = T | null; type Keys = keyof Point; type Tree = { value: T; children: Tree[] }; // recursive is OK Aliases can name anything: unions, primitives, tuples, functions, conditional and mapped types. Interfaces can only describe object shapes. ================================================================================ INTERFACE VS TYPE ================================================================================ DESCRIPTION Both can describe object shapes, be generic, and be implemented. Only interface merges. Only type aliases unions, mapped types, and infer. ------------------------------------------------------------------------------- EXAMPLES ------------------------------------------------------------------------------- Both can: describe object shapes be extended (extends vs intersection) be implemented by a class be generic Only interface can: merge across declarations (module augmentation) produce slightly better error messages in some editors Only type can: alias a union, primitive, tuple, or function type directly use conditional, mapped, and template literal types use the infer keyword Practical rule: use interface for object shapes that other code implements or extends, especially public API surfaces. Use type for everything else, particularly unions and computed types. Consistency within a codebase matters more than the choice. ================================================================================ UNIONS AND INTERSECTIONS ================================================================================ DESCRIPTION A union is A | B (one of). An intersection is A & B (both). Unions of objects usually need a discriminant to narrow. ------------------------------------------------------------------------------- UNIONS ------------------------------------------------------------------------------- type Result = string | number; type Status = "idle" | "loading" | "error"; Only members common to every branch are accessible without narrowing: function f(x: string | number) { x.toString(); // fine, both have it x.toUpperCase(); // error, number does not } ------------------------------------------------------------------------------- INTERSECTIONS ------------------------------------------------------------------------------- type Timestamped = { createdAt: Date }; type User = { name: string }; type StampedUser = User & Timestamped; // has both An intersection of conflicting primitive types is never: type Impossible = string & number; // never ------------------------------------------------------------------------------- UNION OF OBJECTS ------------------------------------------------------------------------------- type Shape = | { kind: "circle"; radius: number } | { kind: "square"; side: number }; This is a discriminated union; see that section. It is the single most useful pattern in the language. ------------------------------------------------------------------------------- DISTRIBUTIVITY ------------------------------------------------------------------------------- A conditional type over a naked type parameter distributes across a union: type ToArray = T extends any ? T[] : never; type R = ToArray; // string[] | number[] Prevent distribution by wrapping in a tuple: type NoDist = [T] extends [any] ? T[] : never; type R2 = NoDist; // (string | number)[] ================================================================================ LITERAL TYPES ================================================================================ DESCRIPTION A type that is one specific string, number, or boolean rather than the widened primitive. as const and unions of literals are the usual sources. ------------------------------------------------------------------------------- CREATE ------------------------------------------------------------------------------- type Direction = "up" | "down" | "left" | "right"; type Dice = 1 | 2 | 3 | 4 | 5 | 6; type Flag = true; let d: Direction = "up"; d = "sideways"; // error Widening: let a = "up"; // string, widened const b = "up"; // "up", literal let c: Direction = "up"; // "up", annotated Deriving a union from a runtime array keeps one source of truth: const DIRECTIONS = ["up", "down"] as const; type Direction = typeof DIRECTIONS[number]; This pattern is usually better than an enum; see the next section. ================================================================================ ENUMS ================================================================================ DESCRIPTION TypeScript enums exist at runtime (except const enum). A union of string literals plus as const is usually the better choice. ------------------------------------------------------------------------------- CREATE ------------------------------------------------------------------------------- enum Status { Idle, Loading, Done } // 0, 1, 2 enum Level { Low = 1, Mid = 5, High = 10 } enum Role { Admin = "ADMIN", User = "USER" } ------------------------------------------------------------------------------- WHY TO AVOID THEM ------------------------------------------------------------------------------- Enums are one of the few TypeScript features that emit runtime code, which breaks the "types are erased" model and prevents isolatedModules tooling from stripping types cleanly. Numeric enums also accept any number in older versions, and are not type-safe in the way the name suggests. The modern replacement: const Role = { Admin: "ADMIN", User: "USER" } as const; type Role = typeof Role[keyof typeof Role]; // "ADMIN" | "USER" You get autocomplete, exhaustiveness, no runtime object beyond a plain frozen literal, and the values are just strings in the JSON you send. ------------------------------------------------------------------------------- CONST ENUMS ------------------------------------------------------------------------------- const enum Direction { Up, Down } Inlined at the call site with no runtime object, but incompatible with isolatedModules and with most bundler setups. Avoid. ------------------------------------------------------------------------------- INTERVIEW NOTES ------------------------------------------------------------------------------- "When would you use an enum" is a reasonable filter question. The strong answer names the runtime-emit problem and the as-const union alternative, rather than treating enums as the obvious C#-style choice. ================================================================================ NARROWING ================================================================================ DESCRIPTION Narrowing is how TypeScript refines a broad type to a specific one inside a block. Control flow analysis does this automatically. ------------------------------------------------------------------------------- TYPEOF ------------------------------------------------------------------------------- function f(x: string | number) { if (typeof x === "string") x.toUpperCase(); else x.toFixed(2); } Remember typeof null is "object", so a typeof check does not exclude null from an object type. ------------------------------------------------------------------------------- TRUTHINESS ------------------------------------------------------------------------------- function f(s?: string) { if (s) s.length; // string } Careful: this also excludes "" and 0, which may be valid values. Prefer an explicit check when they are: if (s !== undefined) { } ------------------------------------------------------------------------------- EQUALITY ------------------------------------------------------------------------------- function f(x: string | number, y: string | boolean) { if (x === y) { // both narrowed to string } } if (x != null) { } // removes both null and undefined ------------------------------------------------------------------------------- INSTANCEOF ------------------------------------------------------------------------------- if (err instanceof ValidationError) err.field; ------------------------------------------------------------------------------- IN OPERATOR ------------------------------------------------------------------------------- type Fish = { swim(): void }; type Bird = { fly(): void }; function move(animal: Fish | Bird) { if ("swim" in animal) animal.swim(); else animal.fly(); } ------------------------------------------------------------------------------- ARRAY.ISARRAY ------------------------------------------------------------------------------- function f(x: string | string[]) { if (Array.isArray(x)) x.join(","); else x.toUpperCase(); } ------------------------------------------------------------------------------- ASSIGNMENT AND CONTROL FLOW ------------------------------------------------------------------------------- let x: string | number = "a"; x.toUpperCase(); // narrowed to string by assignment x = 42; x.toFixed(); // now number Early return narrows the rest of the function: function f(u: User | null) { if (!u) return; u.name; // User } ------------------------------------------------------------------------------- WHERE NARROWING IS LOST ------------------------------------------------------------------------------- Inside a callback, because TypeScript cannot know when it runs: if (this.user) { setTimeout(() => this.user.name); // error } const user = this.user; if (user) setTimeout(() => user.name); // fine After any function call, if the narrowed value is a mutable property of an object, since the call could have reassigned it. Copy to a const first; this is the standard fix. ================================================================================ TYPE GUARDS ================================================================================ DESCRIPTION A function whose return type is a type predicate (x is T) or an assertion (asserts x is T). That is how you teach the checker a custom check. ------------------------------------------------------------------------------- CREATE ------------------------------------------------------------------------------- function isString(x: unknown): x is string { return typeof x === "string"; } function isUser(x: unknown): x is User { return ( typeof x === "object" && x !== null && "id" in x && typeof (x as User).id === "number" ); } The "x is T" return type tells the compiler to narrow at the call site. It is an assertion you are responsible for getting right; a wrong guard silently lies to the type system. ------------------------------------------------------------------------------- FILTERING WITH A GUARD ------------------------------------------------------------------------------- const maybe: (string | null)[] = ["a", null, "b"]; const strings = maybe.filter((x): x is string => x !== null); // string[] Without the guard annotation, filter returns (string | null)[]. A reusable version: function isDefined(x: T | null | undefined): x is T { return x != null; } const clean = maybe.filter(isDefined); ------------------------------------------------------------------------------- ASSERTION FUNCTIONS ------------------------------------------------------------------------------- function assertIsString(x: unknown): asserts x is string { if (typeof x !== "string") throw new TypeError("expected string"); } function assert(condition: unknown, msg?: string): asserts condition { if (!condition) throw new Error(msg ?? "assertion failed"); } const value: unknown = getValue(); assertIsString(value); value.toUpperCase(); // narrowed from here on An assertion function must have an explicit type annotation on the variable holding it if assigned to a const, which is a known rough edge. ================================================================================ DISCRIMINATED UNIONS ================================================================================ DESCRIPTION A union of object types sharing a common literal-typed property. This is the workhorse pattern of TypeScript and replaces most places where C# would use inheritance and polymorphism. ------------------------------------------------------------------------------- CREATE ------------------------------------------------------------------------------- type Shape = | { kind: "circle"; radius: number } | { kind: "square"; side: number } | { kind: "rect"; w: number; h: number }; function area(shape: Shape): number { switch (shape.kind) { case "circle": return Math.PI * shape.radius ** 2; case "square": return shape.side ** 2; case "rect": return shape.w * shape.h; } } Inside each case, the union is narrowed to exactly one member and its unique properties are accessible. ------------------------------------------------------------------------------- EXHAUSTIVENESS ------------------------------------------------------------------------------- function area(shape: Shape): number { switch (shape.kind) { case "circle": return Math.PI * shape.radius ** 2; case "square": return shape.side ** 2; default: { const _exhaustive: never = shape; throw new Error(`unhandled kind: ${JSON.stringify(shape)}`); } } } Adding a new member to Shape now fails to compile at every switch that does not handle it. This is the main reason to prefer unions over optional-property grab-bag objects. ------------------------------------------------------------------------------- ASYNC STATE ------------------------------------------------------------------------------- type State = | { status: "idle" } | { status: "loading" } | { status: "success"; data: T } | { status: "error"; error: Error }; This makes impossible states impossible: you cannot have data and an error at the same time, and you cannot read data without first checking the status. ------------------------------------------------------------------------------- RESULT TYPE ------------------------------------------------------------------------------- type Result = | { ok: true; value: T } | { ok: false; error: E }; async function safeFetch(url: string): Promise> { try { const res = await fetch(url); if (!res.ok) return { ok: false, error: new Error(`${res.status}`) }; return { ok: true, value: (await res.json()) as T }; } catch (err) { return { ok: false, error: err as Error }; } } const result = await safeFetch("/api/me"); if (result.ok) use(result.value); else handle(result.error); ------------------------------------------------------------------------------- COMMON MISTAKES ------------------------------------------------------------------------------- Using a boolean as the discriminant with more than two states, or using a non-literal type (string instead of "circle" | "square"), which defeats narrowing entirely. Building one object type with many optional properties instead of a union, then checking for undefined everywhere. The union expresses the actual states. ================================================================================ FUNCTIONS ================================================================================ DESCRIPTION Parameter, return, this, overload, and rest typing. Annotate the function type or the parameters; do not do both unless they disagree on purpose. ------------------------------------------------------------------------------- CREATE ------------------------------------------------------------------------------- function add(a: number, b: number): number { return a + b; } const add = (a: number, b: number): number => a + b; type BinaryOp = (a: number, b: number) => number; const add: BinaryOp = (a, b) => a + b; // params inferred from type Annotating the variable rather than the parameters is often cleaner and is called contextual typing. ------------------------------------------------------------------------------- OPTIONAL, DEFAULT, REST ------------------------------------------------------------------------------- function f(a: number, b?: number): void { } // b: number | undefined function g(a: number, b: number = 10): void { } // b: number function h(...nums: number[]): number { } function k(first: string, ...rest: number[]): void { } Optional parameters must follow required ones. ------------------------------------------------------------------------------- OBJECT PARAMETERS ------------------------------------------------------------------------------- interface CreateUserOptions { name: string; age?: number; isAdmin?: boolean; } function createUser({ name, age = 0, isAdmin = false }: CreateUserOptions) { } This is the TypeScript equivalent of C# named and optional arguments, and the standard shape for any function with more than three inputs. ------------------------------------------------------------------------------- OVERLOADS ------------------------------------------------------------------------------- function parse(input: string): object; function parse(input: string, asArray: true): unknown[]; function parse(input: string, asArray?: boolean): object | unknown[] { const result = JSON.parse(input); return asArray ? [result].flat() : result; } The implementation signature is not callable from outside; only the overload signatures are. Prefer a union return or generics when they express the same thing, since overloads are harder to maintain. ------------------------------------------------------------------------------- THIS PARAMETER ------------------------------------------------------------------------------- function handler(this: HTMLButtonElement, e: MouseEvent): void { this.disabled = true; } A fake first parameter, erased at compile time, that types this. ------------------------------------------------------------------------------- RETURN TYPE NOTES ------------------------------------------------------------------------------- Let return types be inferred inside a module; annotate them on exported functions. An annotation is a contract that catches accidental widening and produces better errors at the definition rather than at every call site. A function returning a type predicate (x is T) or asserts must always be annotated; it cannot be inferred. ================================================================================ CLASSES ================================================================================ DESCRIPTION Typed fields, parameter properties, visibility, abstract classes, implements, and generic classes. strictPropertyInitialization is the usual gotcha. ------------------------------------------------------------------------------- CREATE ------------------------------------------------------------------------------- class User { readonly id: number; name: string; protected role: string = "user"; private secret: string; static count = 0; #trulyPrivate = 1; // runtime private, not just compile constructor(id: number, name: string, secret: string) { this.id = id; this.name = name; this.secret = secret; User.count++; } greet(): string { return `Hi, ${this.name}`; } get display(): string { return this.name.toUpperCase(); } set display(v: string) { this.name = v.toLowerCase(); } } ------------------------------------------------------------------------------- PARAMETER PROPERTIES ------------------------------------------------------------------------------- class User { constructor( public readonly id: number, public name: string, private secret: string ) {} } A modifier on a constructor parameter declares and assigns the field in one step. This is TypeScript-only syntax with no JavaScript equivalent. ------------------------------------------------------------------------------- ACCESS MODIFIERS ------------------------------------------------------------------------------- public default, accessible anywhere protected this class and subclasses private this class only, COMPILE TIME ONLY #field true runtime privacy, enforced by the engine readonly assignable only in the constructor or initializer private is erased. At runtime the property is a normal property and can be reached with obj["secret"]. Use #field when privacy must actually hold, for example when handling secrets or preventing external tampering. ------------------------------------------------------------------------------- ABSTRACT CLASSES ------------------------------------------------------------------------------- abstract class Shape { abstract area(): number; abstract readonly kind: string; describe(): string { return `${this.kind}: ${this.area()}`; } } class Circle extends Shape { readonly kind = "circle"; constructor(private r: number) { super(); } area(): number { return Math.PI * this.r ** 2; } } Unlike interfaces, abstract classes exist at runtime and can hold implementation. Unlike C#, you usually reach for a discriminated union instead. ------------------------------------------------------------------------------- IMPLEMENTS AND OVERRIDE ------------------------------------------------------------------------------- class Service implements IService { } class Derived extends Base { override handle(): void { } // required with noImplicitOverride } ------------------------------------------------------------------------------- GENERIC CLASSES ------------------------------------------------------------------------------- class Repository { #items = new Map(); add(item: T): void { this.#items.set(item.id, item); } find(id: number): T | undefined { return this.#items.get(id); } all(): T[] { return [...this.#items.values()]; } } const users = new Repository(); ------------------------------------------------------------------------------- STRICT PROPERTY INITIALIZATION ------------------------------------------------------------------------------- Under strict, every declared field must be assigned in the constructor or have an initializer. When it is assigned elsewhere (a framework, a lifecycle hook), use the definite assignment assertion: class C { value!: string; // "trust me, it gets set" } Use this sparingly; it is a promise the compiler cannot check. ------------------------------------------------------------------------------- COMMON MISTAKES ------------------------------------------------------------------------------- Assuming private means private at runtime. It does not. Reaching for class hierarchies out of C# habit where a discriminated union or a factory function would be simpler and easier to serialize. Forgetting that classes are compared structurally, so two unrelated classes with the same shape are mutually assignable. A private field is the usual way to force nominal-like behavior: class Brand { private __brand?: never; } ================================================================================ GENERICS ================================================================================ DESCRIPTION A type parameter is a placeholder the caller fills in. Constrain it when you need properties; default it when the common case is obvious. ------------------------------------------------------------------------------- CREATE ------------------------------------------------------------------------------- function identity(value: T): T { return value; } identity("a"); // explicit identity("a"); // inferred, preferred Multiple parameters: function pair(key: K, value: V): [K, V] { return [key, value]; } ------------------------------------------------------------------------------- CONSTRAINTS ------------------------------------------------------------------------------- function longest(a: T, b: T): T { return a.length >= b.length ? a : b; } longest("abc", "de"); // string longest([1, 2], [3]); // number[] longest(1, 2); // error, number has no length ------------------------------------------------------------------------------- KEY CONSTRAINTS ------------------------------------------------------------------------------- function getProp(obj: T, key: K): T[K] { return obj[key]; } const user = { name: "Jake", age: 30 }; getProp(user, "name"); // string getProp(user, "nope"); // error This is the single most useful generic pattern in application code. ------------------------------------------------------------------------------- DEFAULTS ------------------------------------------------------------------------------- interface ApiResponse { data: T; status: number; } type Result = { ok: true; value: T } | { ok: false; error: E }; ------------------------------------------------------------------------------- GENERIC CONSTRAINTS ON CONSTRUCTORS ------------------------------------------------------------------------------- type Constructor = new (...args: any[]) => T; function Timestamped(Base: TBase) { return class extends Base { createdAt = new Date(); }; } class User {} const TimestampedUser = Timestamped(User); This is the mixin pattern, TypeScript's answer to multiple inheritance. ------------------------------------------------------------------------------- WHEN NOT TO USE GENERICS ------------------------------------------------------------------------------- A type parameter used only once in the signature is usually a mistake: function f(x: T): void { } // T adds nothing, use unknown The rule of thumb: a generic earns its place when it relates two or more positions (a parameter to the return type, or two parameters to each other). ------------------------------------------------------------------------------- COMMON MISTAKES ------------------------------------------------------------------------------- Expecting T to exist at runtime. It does not; types are erased. If you need runtime behavior per type, pass a value (a schema, a constructor, a tag string). Over-constraining with any: is not a constraint. Adding explicit type arguments everywhere when inference already works, which makes refactoring harder. ================================================================================ KEYOF TYPEOF INDEXED ACCESS ================================================================================ DESCRIPTION keyof T is the union of keys. typeof value captures a type from a value. T[K] looks up a property type. Together they type dynamic access. ------------------------------------------------------------------------------- KEYOF ------------------------------------------------------------------------------- interface User { id: number; name: string; } type UserKey = keyof User; // "id" | "name" type AnyKey = keyof any; // string | number | symbol ------------------------------------------------------------------------------- TYPEOF (TYPE POSITION) ------------------------------------------------------------------------------- const config = { host: "localhost", port: 3000 }; type Config = typeof config; // { host: string; port: number } type ConfigKey = keyof typeof config; // "host" | "port" typeof in a type position queries the type of a VALUE. It is unrelated to the runtime typeof operator despite the shared keyword. ------------------------------------------------------------------------------- INDEXED ACCESS ------------------------------------------------------------------------------- type Name = User["name"]; // string type Either = User["id" | "name"]; // number | string type Values = User[keyof User]; // number | string Array element type: type Item = typeof items[number]; Function return and parameter types: type R = ReturnType; type P = Parameters[0]; ------------------------------------------------------------------------------- THE AS-CONST PIPELINE ------------------------------------------------------------------------------- const ROLES = ["admin", "editor", "viewer"] as const; type Role = typeof ROLES[number]; // "admin"|"editor"|"viewer" const ROUTES = { home: "/", user: "/users/:id" } as const; type RouteName = keyof typeof ROUTES; // "home" | "user" type RoutePath = typeof ROUTES[RouteName]; // "/" | "/users/:id" One runtime source of truth, types derived from it. This combination is worth memorizing; it comes up constantly. ================================================================================ CONDITIONAL TYPES ================================================================================ DESCRIPTION T extends U ? X : Y. Combined with infer they extract types from other types. They distribute over naked unions. ------------------------------------------------------------------------------- CREATE ------------------------------------------------------------------------------- type IsString = T extends string ? true : false; type A = IsString<"hello">; // true type B = IsString<42>; // false ------------------------------------------------------------------------------- INFER ------------------------------------------------------------------------------- infer captures a type from within a pattern: type ElementOf = T extends (infer U)[] ? U : never; type E = ElementOf; // string type Unwrap = T extends Promise ? U : T; type U = Unwrap>; // number type Ret = T extends (...args: any[]) => infer R ? R : never; type First = T extends [infer F, ...any[]] ? F : never; ------------------------------------------------------------------------------- PRACTICAL EXAMPLES ------------------------------------------------------------------------------- Deep partial: type DeepPartial = T extends object ? { [K in keyof T]?: DeepPartial } : T; Non-nullable: type NonNull = T extends null | undefined ? never : T; Function arity: type Awaited1 = T extends PromiseLike ? Awaited1 : T; Filtering a union: type Extract2 = T extends U ? T : never; type Exclude2 = T extends U ? never : T; ------------------------------------------------------------------------------- NOTE ------------------------------------------------------------------------------- Conditional types are where TypeScript becomes a small functional language. They are essential for library authors and rarely needed in application code. If a conditional type in application code is more than three lines, there is usually a simpler shape available. ================================================================================ MAPPED TYPES ================================================================================ DESCRIPTION Build a new object type by iterating keys: { [K in keyof T]: ... }. The as clause remaps keys. This is how Partial, Pick, and Readonly are written. ------------------------------------------------------------------------------- CREATE ------------------------------------------------------------------------------- type Optional = { [K in keyof T]?: T[K] }; type Immutable = { readonly [K in keyof T]: T[K] }; type Stringify = { [K in keyof T]: string }; ------------------------------------------------------------------------------- MODIFIERS ------------------------------------------------------------------------------- Add with + (implicit), remove with -: type Mutable = { -readonly [K in keyof T]: T[K] }; type Required2 = { [K in keyof T]-?: T[K] }; ------------------------------------------------------------------------------- KEY REMAPPING WITH AS ------------------------------------------------------------------------------- type Getters = { [K in keyof T as `get${Capitalize}`]: () => T[K] }; interface User { name: string; age: number } type UserGetters = Getters; // { getName: () => string; getAge: () => number } Filter keys by remapping to never: type OnlyStrings = { [K in keyof T as T[K] extends string ? K : never]: T[K] }; ------------------------------------------------------------------------------- MAPPING A UNION OF KEYS ------------------------------------------------------------------------------- type Flags = { [K in "read" | "write"]: boolean }; // { read: boolean; write: boolean } type Record2 = { [P in K]: V }; ================================================================================ TEMPLATE LITERAL TYPES ================================================================================ DESCRIPTION String types concatenated and inferred at the type level. Useful for event names, CSS-ish unions, and extracting pieces of literal strings. ------------------------------------------------------------------------------- EXAMPLES ------------------------------------------------------------------------------- type Greeting = `hello ${string}`; type Event = `on${"Click" | "Hover"}`; // "onClick" | "onHover" type CssUnit = `${number}px` | `${number}rem`; Intrinsic string manipulation types: Uppercase<"abc"> // "ABC" Lowercase<"ABC"> // "abc" Capitalize<"abc"> // "Abc" Uncapitalize<"Abc"> // "abc" Practical: typed event names and route params. type Route = "/users/:id/posts/:postId"; type Params = T extends `${string}:${infer P}/${infer Rest}` ? P | Params<`/${Rest}`> : T extends `${string}:${infer P}` ? P : never; type R = Params; // "id" | "postId" Powerful, but heavy. Deep recursive template types slow the compiler noticeably in large projects. ================================================================================ UTILITY TYPES ================================================================================ DESCRIPTION The built-in Partial, Required, Pick, Omit, Record, Exclude, Extract, NonNullable, ReturnType, Parameters, Awaited, and a few you will write yourself. ------------------------------------------------------------------------------- COMMON METHODS ------------------------------------------------------------------------------- Partial all properties optional Required all properties required Readonly all properties readonly Pick keep only keys K Omit remove keys K Record object with keys K and values V interface User { id: number; name: string; email: string } type UserDraft = Partial; type UserSummary = Pick; type PublicUser = Omit; type UsersById = Record; type Flags = Record<"read" | "write", boolean>; ------------------------------------------------------------------------------- UNION TRANSFORMS ------------------------------------------------------------------------------- Exclude remove members of T assignable to U Extract keep members of T assignable to U NonNullable remove null and undefined type Status = "idle" | "loading" | "error"; type Settled = Exclude; // "idle" | "error" ------------------------------------------------------------------------------- FUNCTION TYPES ------------------------------------------------------------------------------- Parameters tuple of parameter types ReturnType return type ConstructorParameters InstanceType ThisParameterType OmitThisParameter Awaited unwrap nested promises type Args = Parameters; type Res = Awaited>; The Awaited> combination is how you get the resolved type of an async function without exporting a separate type. ------------------------------------------------------------------------------- STRING TYPES ------------------------------------------------------------------------------- Uppercase Lowercase Capitalize Uncapitalize ------------------------------------------------------------------------------- USEFUL CUSTOM UTILITIES ------------------------------------------------------------------------------- Deep readonly: type DeepReadonly = { readonly [K in keyof T]: T[K] extends object ? DeepReadonly : T[K] }; At least one key required: type RequireAtLeastOne = Omit & { [P in K]-?: Required> }[K]; Prettify (flattens intersections in tooltips, purely cosmetic but very useful when debugging types): type Prettify = { [K in keyof T]: T[K] } & {}; Nominal branding: type Brand = T & { readonly __brand: B }; type UserId = Brand; type PostId = Brand; // UserId and PostId are no longer interchangeable ------------------------------------------------------------------------------- COMMON MISTAKES ------------------------------------------------------------------------------- Omit does not check that K is a key of T, so a typo silently omits nothing. Pick does check. This is a known wart. Using Partial on a function parameter and then treating every field as present. Partial makes everything optional, including things your code requires. ================================================================================ TYPE ASSERTIONS ================================================================================ DESCRIPTION as T and postfix ! tell the checker to stop. Nothing happens at runtime. Prefer a guard; comment the assertion when you cannot. ------------------------------------------------------------------------------- EXAMPLES ------------------------------------------------------------------------------- const el = document.getElementById("app") as HTMLCanvasElement; const el = document.getElementById("app"); // not in .tsx An assertion tells the compiler to stop checking. It does not convert anything at runtime. If you are wrong, it fails later and further away. Non-null assertion: const el = document.getElementById("app")!; user!.profile!.email; Double assertion when the types do not overlap (a code smell, but sometimes necessary in tests): const mock = {} as unknown as Database; Prefer, in order: 1. fix the types so no assertion is needed 2. a type guard that actually checks 3. an assertion with a comment explaining why it is safe ================================================================================ SATISFIES ================================================================================ DESCRIPTION satisfies checks a value against a type without widening the value's inferred type. Use it for config objects and as const tables you still want checked. ------------------------------------------------------------------------------- EXAMPLES ------------------------------------------------------------------------------- satisfies checks a value against a type WITHOUT widening it. const config = { host: "localhost", port: 3000 } satisfies Record; config.port.toFixed(); // number, still narrow Compare: const a: Record = { host: "x", port: 1 }; a.port.toFixed(); // error, widened to string | number The common use: validate a config or route map against a shape while keeping the literal types for autocomplete. const routes = { home: "/", user: "/users/:id" } satisfies Record; type RouteName = keyof typeof routes; // "home" | "user" Rule of thumb: annotation for variables you will reassign, satisfies for constant data you want both validated and precisely typed. ================================================================================ MODULES AND DECLARATION FILES ================================================================================ DESCRIPTION Type-only imports, .d.ts files, module augmentation, and env typing. isolatedModules wants import type when you only need the type. ------------------------------------------------------------------------------- CREATE ------------------------------------------------------------------------------- import type { User } from "./types"; import { type User, createUser } from "./api"; export type { User }; The type modifier guarantees the import is erased at compile time. It is required under isolatedModules when importing a type from a module whose runtime side you do not need, and it prevents accidental circular runtime dependencies. ------------------------------------------------------------------------------- DECLARATION FILES ------------------------------------------------------------------------------- A .d.ts file contains types only, no implementation. // types/env.d.ts declare module "*.svg" { const content: string; export default content; } declare global { interface Window { analytics: Analytics; } } export {}; // makes the file a module, required for declare global ------------------------------------------------------------------------------- THIRD PARTY TYPES ------------------------------------------------------------------------------- npm install -D @types/express npm install -D @types/node Modern packages ship their own types via a "types" field in package.json. @types/* packages from DefinitelyTyped cover the rest. For an untyped package, a minimal stub: // types/untyped-lib.d.ts declare module "untyped-lib" { export function doThing(input: string): number; } ------------------------------------------------------------------------------- MODULE AUGMENTATION ------------------------------------------------------------------------------- Add properties to an existing third-party type: // types/express.d.ts import "express"; declare module "express-serve-static-core" { interface Request { user?: { id: number; role: string }; } } This is why interfaces support declaration merging and type aliases do not. ------------------------------------------------------------------------------- ENVIRONMENT VARIABLES ------------------------------------------------------------------------------- declare global { namespace NodeJS { interface ProcessEnv { DATABASE_URL: string; PORT?: string; NODE_ENV: "development" | "production" | "test"; } } } This gives autocomplete on process.env but does NOT validate at runtime. Validate with a schema at startup; see RUNTIME VALIDATION. ================================================================================ ASYNC TYPES ================================================================================ DESCRIPTION async functions return Promise. res.json() is Promise; assign it to unknown and validate. Awaited unwraps promises. ------------------------------------------------------------------------------- EXAMPLES ------------------------------------------------------------------------------- async function loadUser(id: number): Promise { const res = await fetch(`/api/users/${id}`); if (!res.ok) throw new Error(`HTTP ${res.status}`); return res.json() as Promise; } An async function must be annotated Promise, not T. Typing fetch results honestly: async function loadUser(id: number): Promise { const res = await fetch(`/api/users/${id}`); const data: unknown = await res.json(); return parseUser(data); // a real validator, see below } res.json() returns Promise, which silently poisons everything downstream. Assign it to unknown and validate. Promise combinators keep their tuple types: const [user, posts] = await Promise.all([ loadUser(1), loadPosts(1) ]); // [User, Post[]] const results = await Promise.allSettled([loadUser(1)]); // PromiseSettledResult[] Unwrapping: type UserType = Awaited>; // User ================================================================================ ERROR HANDLING ================================================================================ DESCRIPTION catch (err) is unknown under strict. Narrow with instanceof, or normalize to Error. Typed failure belongs in the return type (a Result union), not in a throws clause — TypeScript has none. ------------------------------------------------------------------------------- EXAMPLES ------------------------------------------------------------------------------- With useUnknownInCatchVariables (part of strict), catch is unknown: try { risky(); } catch (err) { if (err instanceof ValidationError) handle(err.field); else if (err instanceof Error) log(err.message); else log(String(err)); } A reusable normalizer: function toError(err: unknown): Error { if (err instanceof Error) return err; return new Error(typeof err === "string" ? err : JSON.stringify(err)); } Custom error classes: class HttpError extends Error { constructor( message: string, public readonly status: number, options?: ErrorOptions ) { super(message, options); this.name = "HttpError"; } } When targeting ES5, extending built-ins like Error breaks instanceof. Target ES2015 or later, or add: Object.setPrototypeOf(this, HttpError.prototype); Typed result instead of throwing: see the Result type in DISCRIMINATED UNIONS. TypeScript has no checked exceptions and no way to express "this function throws E", so a Result union is the only way to make failure visible in the signature. ================================================================================ REACT ================================================================================ DESCRIPTION Props, native element extensions, hook types, events, and generic components. The component's props type is the whole public API. ------------------------------------------------------------------------------- CREATE ------------------------------------------------------------------------------- interface ButtonProps { label: string; variant?: "primary" | "secondary"; disabled?: boolean; onClick: (event: React.MouseEvent) => void; children?: React.ReactNode; } function Button({ label, variant = "primary", onClick }: ButtonProps) { return ; } Prefer a plain annotated function over React.FC. React.FC adds an implicit children prop (in older versions), complicates generics, and buys nothing. ------------------------------------------------------------------------------- EXTENDING NATIVE ELEMENT PROPS ------------------------------------------------------------------------------- interface ButtonProps extends React.ButtonHTMLAttributes { variant?: "primary" | "secondary"; } function Button({ variant = "primary", ...rest }: ButtonProps) { return