TypeScript Cheatsheet 0/19

Backend engineeringTypeScript 5.xTypes you can run

TypeScript
Cheatsheet

Twenty modules on the type system, from a first annotation to mapped and conditional types. Press Run on any example and it compiles in your browser and types its output into the terminal below it.

TypeScript 5.6Runs in the browser
00

Key terms in plain words

Never written TypeScript, or any code at all? Start here. Every word below shows up later on this page, each with an everyday comparison and a plain explanation. Underlined words in the modules link back to these cards.

35 terms in 7 groups. Hover an underlined word anywhere on the page for a quick reminder, or click it to jump here.

What TypeScript adds

TypeScript works like a post office counter. Every parcel carries a label saying what's inside, and a clerk checks each label before anything goes out.

TypeScript
JavaScript with labels added. You write what kind of value each name holds, a checker reads the whole program looking for mismatches, and then the labels are stripped off to leave ordinary JavaScript.
Type
A description of what a value is allowed to be: a number, some text, a list, an object with certain fields. TypeScript uses types to catch you putting the wrong thing in the wrong place.
For example string, number and boolean are the simplest types.
Compiler, compile time and runtime
The compiler (tsc) reads your code, reports anything that doesn't fit its types, and writes out plain JavaScript. That checking moment is compile time. Runtime is later, when the program actually runs, and by then most of TypeScript has vanished.
Type annotation
A colon and a type after a name, telling TypeScript what that name holds. It is removed when the code compiles, so it costs nothing while the program runs.
For example let count: number = 0
Primitive
The simplest kinds of value: string (text), number, boolean (true or false), bigint, symbol, null and undefined. Every other type is built out of these.

The everyday labels

Lists, fixed packs, open boxes, and the labels the clerk writes for you.

Array
An ordered list of values of any length. string[] means a list where every item is text.
For example const tags: string[] = ["types", "generics"]
Tuple
A list with a fixed length where every position has its own type. Use one when position carries meaning, like the x and y of a point.
For example [x: number, y: number]
any, unknown and never
any switches checking off for a value. unknown also accepts anything, but makes you check what it is before you use it. never is the type of something that can't happen, such as the result of a function that always throws an error.
Type inference
TypeScript works out a type from the value you give it, so you rarely need to annotate a local variable yourself.
For example let count = 10 is already known to be a number.
Literal type
A type that allows a single exact value, like "dark" or 3. const keeps that exact value as the type, while let widens it to the general one (string, number) because it might change later.
as const
Written after a value, it makes every part of it readonly and keeps every exact literal. It's the usual way to turn a plain list into a fixed set of allowed values.
For example ["GET", "POST"] as const

One of several things

Saying a value could be this or that, then finding out which one you were handed.

Union type
A type that allows one of several options, written with |. Until you check which option you have, you can only use what all of them share.
For example type Id = string | number
Intersection
Written with &, it joins two types into one that has everything from both.
For example User & Timestamps has a name and a creation date.
Narrowing
Checking a value inside an if or switch so that, in that branch, TypeScript knows its exact type. typeof, in and instanceof checks all narrow.
Discriminated union
A union of object types that all share one field, often kind, with a different exact value in each. Checking that one field tells TypeScript which shape you're holding.
For example { kind: "circle"; radius: number } | { kind: "square"; side: number }

Naming your shapes

Writing a label once, giving it a name, and reusing it on every parcel of that kind.

Interface
A named description of an object: which fields it has and the type of each. One interface can extend another, and two interfaces with the same name merge into one.
For example interface User { id: number; name: string }
Type alias
type Name = ... gives a name to any type, not only objects. Unions, tuples and computed types can only be named this way.
For example type Status = "idle" | "loading" | "done"
Optional and readonly
A ? after a field name means it can be missing. readonly means it is set once and can't be changed afterwards. Both are checked by the compiler only.
For example email?: string and readonly id: number
Enum
A named set of constants that, unlike most of TypeScript, still exists when the program runs. Numeric enums count up from 0; string enums give each member a readable value.
For example enum Role { Admin = "admin", Viewer = "viewer" }
Overload
Several call signatures written above a single function body, so each kind of input gets its own precise return type.
For example parse("42") returns a number, parse(7) returns a string.
Class
A blueprint for making objects that share the same fields and methods. Words like public, protected and private say who may touch each member, and TypeScript checks them before the code runs.
For example new Rect(3, 4) builds one object from the Rect class.

Types that build types

Labels with a blank in them, and tools that read one label to write another.

Generic
A type or function with a placeholder, usually called T, that gets filled in each time it's used. It keeps the link between what goes in and what comes out.
For example first<T>(items: T[]): T hands back a number from a list of numbers.
Generic constraint
T extends X limits what a type parameter may be, so the code inside can safely use whatever X promises.
For example T extends { length: number } means T must have a length.
keyof and typeof
keyof gives the union of an object type's keys. typeof (in a type) reads the type TypeScript worked out for a value, so you write the value once and reuse its shape.
For example keyof { id: number; name: string } is "id" | "name".
Indexed access
T[K] reads the type of one field of T. T[number] reads the type of the items in an array.
For example Config["db"] is the type of the db field.
Mapped type
Loops over the keys of a type and builds a new one from them, so you can make every field optional, readonly or nullable in one line.
For example { [K in keyof T]: T[K] | null }
Conditional type and infer
T extends U ? X : Y picks one type or another depending on whether T fits U, the way ? : picks a value. Inside it, infer captures a piece of the matched type, such as the value a promise holds.
Template literal type
A backtick string at the type level. With unions inside, it expands to every combination. With ${string} inside, it describes a pattern any matching text fits.
For example The pattern ${number}px accepts "240px" but not "wide".
Utility types
Generics that come with TypeScript and reshape a type for you. Partial makes every field optional, Pick and Omit keep or drop fields, Record builds an object type, ReturnType reads what a function returns.
For example Omit<User, "password"> is a User without its password.

Checks and escape hatches

Teaching the clerk new checks, and the ways of telling it to trust you.

Type guard
A function that returns value is Cat. When it says true, TypeScript narrows the value to that type, even inside filter. An assertion function (asserts) does the same job but throws an error when the check fails.
satisfies
value satisfies Type checks a value against a type but keeps the value's own, more precise type. Compare as, which only tells the checker "trust me" and checks nothing.
For example const palette = { coral: "#FF4D6D" } satisfies Theme
Branded type
A string or number marked with a tag that exists only for the checker. A UserId and an OrderId are both text, but the brand stops you passing one where the other belongs.

Files and settings

How types travel between files, and how you tell the clerk how strict to be.

Import and export
Each file is a module that shares things with export and receives them with import. import type brings in types only and disappears completely when the code compiles.
Declaration file (.d.ts)
A file that holds only types, describing code TypeScript can't see for itself, such as a plain JavaScript library. The declare keyword says something exists without creating it.
For example declare const __APP_VERSION__: string;
tsconfig
tsconfig.json is the compiler's settings file: which JavaScript version to write out, how to find modules, and how strict to be. "strict": true turns on the checks that catch the most bugs.
01

Basic types

Put a colon and a type after a name. Primitives, arrays, tuples and four special types cover most of what you write every day.

PrimitivesArraysTuplesany, unknown, never

The annotation sits after the name: let count: number = 0. TypeScript checks every later use against it, then removes it when the file compiles to JavaScript, so annotations cost nothing at runtime.

basics.tsTS
let title: string = "TypeScript cheatsheet";
let modules: number = 20;
let published: boolean = true;
let big: bigint = 9007199254740993n;
let id: symbol = Symbol("id");

const tags: string[] = ["types", "generics"];
const scores: Array<number> = [90, 85];

// A tuple fixes the length and the type at each position
const point: [x: number, y: number] = [10, 20];
const entry: [string, number, boolean?] = ["age", 30];

console.log(typeof title, typeof modules, typeof big);
console.log(tags.length, scores[1]);
console.log(point, entry.length);
Terminaltsx
~/ts-cheatsheet $ npx tsx basics.ts
string number bigint
2 85
[ 10, 20 ] 2

Why it matters: T[] and Array<T> mean the same thing. Reach for a tuple when position carries meaning, like a coordinate or a key and value pair.

any switches the checker off, unknown keeps it on

any accepts everything and lets you call anything on it, so mistakes reach production. unknown also accepts everything, but you must narrow it before use. never is the type of a value that cannot exist, such as the return of a function that always throws.

special.tsTS
let loose: any = "hello";
loose.toFixed(2);              // compiles, then crashes at runtime

let safe: unknown = "hello";
safe.toUpperCase();            // rejected: narrow it first
if (typeof safe === "string") safe.toUpperCase(); // fine

function fail(msg: string): never {
  throw new Error(msg);
}
function log(msg: string): void {
  console.log(msg);
}
Terminaltsc
~/ts-cheatsheet $ npx tsc --noEmit --strict special.ts
special.ts(5,1): error TS18046: 'safe' is of type 'unknown'.
Found 1 error in special.ts

Why it matters: take outside data (JSON, request bodies, catch errors) as unknown. The compiler then makes you prove its shape before you touch it.

TypeWhat it holdsUse it for
anyAnything, with no checksA last resort while migrating JavaScript
unknownAnything, checks requiredParsed JSON, errors, untrusted input
neverNothing at allFunctions that throw, exhaustive switches
voidNo useful return valueCallbacks and functions that only do work
null / undefinedAn empty valueOptional data, with strictNullChecks on
objectAny non primitiveRarely; prefer a real shape
02

Inference and literals

You rarely need to annotate a local. TypeScript reads the value, and const or as const keeps the exact literal instead of the wide type.

InferenceLiteral typesas constWidening

With let the compiler widens a value to its general type because it may change later. With const it keeps the exact literal. as const goes further and makes a whole object or array readonly with literal values, which is how you turn a list into a union type.

let widenslet a = "dark"Type is string.
const keepsconst a = "dark"Type is "dark".
inference.tsTS
let count = 10;              // number
const mode = "dark";         // "dark"
let size = "md" as "sm" | "md" | "lg";

const config = { retries: 3, url: "/api" };          // { retries: number; url: string }
const frozen = { retries: 3, url: "/api" } as const; // { readonly retries: 3; readonly url: "/api" }

const methods = ["GET", "POST"] as const;
type Method = (typeof methods)[number];              // "GET" | "POST"
const m: Method = "POST";

console.log(count, mode, size);
console.log(config, frozen.retries);
console.log(methods, m);
Terminaltsx
~/ts-cheatsheet $ npx tsx inference.ts
10 dark md
{ retries: 3, url: '/api' } 3
[ 'GET', 'POST' ] POST

Why it matters: (typeof list)[number] gives you one source of truth. Add a method to the array and the union updates itself.

literals.tsTS
type Align = "left" | "center" | "right";

let align: Align = "left";
align = "middle";
Terminaltsc
~/ts-cheatsheet $ npx tsc --noEmit --strict literals.ts
literals.ts(4,1): error TS2322: Type '"middle"' is not assignable to type 'Align'.
Found 1 error in literals.ts

Why it matters: a union of string literals is the lightest way to restrict a value to a fixed set, and editors autocomplete every option.

03

Unions and narrowing

A union says a value is one of several types. Narrowing is how you find out which one, using checks the compiler understands.

A | BA & BNarrowingDiscriminated unions

Inside a union you can only use what every member shares. Check the value with typeof, in, instanceof or a shared literal field, and inside that branch TypeScript knows the exact type. A never check at the end makes the compiler tell you when a new case is added but not handled.

narrowing.tsTS
type Id = string | number;

function format(id: Id): string {
  if (typeof id === "number") return id.toFixed(0).padStart(5, "0");
  return id.toUpperCase();
}

type Circle = { kind: "circle"; radius: number };
type Square = { kind: "square"; side: number };
type Shape = Circle | Square;          // discriminated by `kind`

function area(s: Shape): number {
  switch (s.kind) {
    case "circle": return Math.round(Math.PI * s.radius ** 2);
    case "square": return s.side ** 2;
    default: {
      const unreachable: never = s;    // breaks the build if a case is missing
      return unreachable;
    }
  }
}

console.log(format(42), format("ab-7"));
console.log(area({ kind: "circle", radius: 2 }), area({ kind: "square", side: 3 }));
Terminaltsx
~/ts-cheatsheet $ npx tsx narrowing.ts
00042 AB-7
13 9

Why it matters: model states as a discriminated union instead of one object with many optional fields. Each state then carries only the data that belongs to it.

intersection.tsTS
type Timestamps = { createdAt: Date };
type User = { name: string };
type SavedUser = User & Timestamps;    // has both sets of fields

const u: SavedUser = { name: "Shree", createdAt: new Date(0) };

type Fish = { swim(): string };
type Bird = { fly(): string };
function move(pet: Fish | Bird) {
  return "swim" in pet ? pet.swim() : pet.fly();
}

function describe(e: Error | string) {
  return e instanceof Error ? `Error: ${e.message}` : e;
}

console.log(u.name, u.createdAt.getTime());
console.log(move({ fly: () => "flap" }));
console.log(describe(new Error("boom")), describe("plain"));
Terminaltsx
~/ts-cheatsheet $ npx tsx intersection.ts
Shree 0
flap
Error: boom plain
CheckNarrows byExample
typeofPrimitive typetypeof x === "string"
inA property exists"swim" in pet
instanceofClass or constructore instanceof Error
Literal fieldA shared discriminants.kind === "circle"
Array.isArrayArraysArray.isArray(v)
TruthinessRemoves null and undefinedif (user) { ... }
neverExhaustivenessconst x: never = s
04

Interfaces and type aliases

Both name an object shape. Interfaces extend and merge; type aliases also name unions, tuples and computed types.

interfacetypeextendsreadonly and optional

Mark a field optional with ? and fixed with readonly. An index signature describes keys you do not know in advance, and it can even take a pattern like `x-${string}`. Declaring the same interface twice merges the two, which is how libraries let you add fields to their types.

shapes.tsTS
interface User {
  readonly id: number;
  name: string;
  email?: string;                   // optional
  [meta: `x-${string}`]: unknown;   // any key that starts with "x-"
}

interface Admin extends User {
  permissions: string[];
}

// Declaration merging: a second block adds to the same interface
interface User {
  active: boolean;
}

type Point = { x: number; y: number };
type Point3D = Point & { z: number };
type Status = "idle" | "loading" | "done";   // only a type alias can name a union

const admin: Admin = { id: 1, name: "Shree", active: true, permissions: ["deploy"], "x-team": "backend" };
const p: Point3D = { x: 1, y: 2, z: 3 };
const s: Status = "done";

console.log(admin.name, admin.permissions, admin["x-team"]);
console.log(p, s);
Terminaltsx
~/ts-cheatsheet $ npx tsx shapes.ts
Shree [ 'deploy' ] backend
{ x: 1, y: 2, z: 3 } done
readonly.tsTS
interface User {
  readonly id: number;
  name: string;
}

const user: User = { id: 1, name: "Shree" };
user.name = "Shri";   // fine
user.id = 2;          // rejected
Terminaltsc
~/ts-cheatsheet $ npx tsc --noEmit --strict readonly.ts
readonly.ts(8,6): error TS2540: Cannot assign to 'id' because it is a read-only property.
Found 1 error in readonly.ts

Why it matters: readonly only exists at compile time. For runtime protection you still need Object.freeze.

Featureinterfacetype
Object shapesYesYes
Unions and tuplesNoYes
Extend another shapeextends& (intersection)
Declaration mergingYesNo
Mapped and conditional typesNoYes
A class can implement itYesYes, if it is an object type

A common rule that works well: use an interface for object shapes other code may extend, and a type alias for everything else.

05

Functions

Type the parameters and the compiler checks every call. Add optional, default and rest parameters, overloads and a typed this.

ParametersReturn typesOverloadsFunction types
x?: T

Optional. The caller may leave it out and you get undefined.

See the example
Optional
x = v

Default. Its type is inferred from the default value.

See the example
Default
...xs: T[]

Rest. Collects the remaining arguments into an array.

See the example
Rest
overloads

Several call signatures over one implementation.

See the example
Overloads
this: T

A fake first parameter that types this inside the body.

See the example
this
functions.tsTS
// Parameter types, a default and an optional parameter, a typed return
function greet(name: string, greeting = "Hello", punct?: string): string {
  return `${greeting}, ${name}${punct ?? "!"}`;
}

// Rest parameters, and a function type you can reuse
const sum = (...nums: number[]): number => nums.reduce((a, b) => a + b, 0);
type BinaryOp = (a: number, b: number) => number;
const multiply: BinaryOp = (a, b) => a * b;

// Overloads: two call signatures, one implementation
function parse(input: string): number;
function parse(input: number): string;
function parse(input: string | number): string | number {
  return typeof input === "string" ? Number(input) : String(input);
}

// A typed `this`
interface Counter { count: number; inc(this: Counter): number }
const counter: Counter = { count: 0, inc() { return ++this.count; } };

console.log(greet("Shree"), greet("Ana", "Hi", "?"));
console.log(sum(1, 2, 3), multiply(4, 5));
console.log(parse("42") + 1, parse(7).length);
counter.inc();
console.log(counter.inc());
Terminaltsx
~/ts-cheatsheet $ npx tsx functions.ts
Hello, Shree! Hi, Ana?
6 20
43 1
2

Why it matters: with overloads the caller sees a precise return type for each input. parse("42") is a number, so + 1 is arithmetic, not string joining.

06

Enums

Named constants that exist at runtime. Read keys and values, assign by a dynamic key, reverse a value back to its name and iterate safely.

Numeric enumsString enumskeyof typeofReverse mapping

An enum compiles to a real object. Numeric members count up from 0 and also get a reverse mapping, so Direction[2] gives back the name. String enums have no reverse mapping, but are easier to read in logs and payloads.

enums.tsTS
enum Direction { Up, Down, Left, Right }              // 0, 1, 2, 3
enum Status { Active = "ACTIVE", Banned = "BANNED" }   // string enum
enum Flag { None = 0, Read = 1 << 0, Write = 1 << 1, All = Read | Write }

console.log(Direction.Up, Direction[2]);   // reverse mapping, numeric enums only
console.log(Status.Active, Flag.All);
console.log(Direction);
console.log(Status);
Terminaltsx
~/ts-cheatsheet $ npx tsx enums.ts
0 Left
ACTIVE 3
{ '0': 'Up', '1': 'Down', '2': 'Left', '3': 'Right', Up: 0, Down: 1, Left: 2, Right: 3 }
{ Active: 'ACTIVE', Banned: 'BANNED' }

Why it matters: the printed objects show why numeric enums need care when you loop over them: the object holds both names and numbers as keys.

Dynamic keys and extraction

keyof typeof Role is the union of the enum's names, and the template type `${Role}` is the union of its values. With those two you can index the enum with a variable, turn an incoming string into a checked member, and use the enum as the keys of an object where every key is required.

enum-keys.tsTS
enum Role { Admin = "admin", Editor = "editor", Viewer = "viewer" }

type RoleKey = keyof typeof Role;      // "Admin" | "Editor" | "Viewer"
type RoleValue = `${Role}`;            // "admin" | "editor" | "viewer"

// Dynamic key access: the key is checked against the enum's names
function fromKey(key: RoleKey): Role {
  return Role[key];
}

// Reverse lookup for a string enum: value back to its name
function keyOf(value: Role): RoleKey {
  return (Object.keys(Role) as RoleKey[]).find((k) => Role[k] === value)!;
}

// Runtime guard: is an incoming string a valid member?
const isRole = (v: string): v is Role =>
  (Object.values(Role) as string[]).includes(v);

// The enum as keys: leave one out and the compiler complains
const limits: Record<Role, number> = {
  [Role.Admin]: 100,
  [Role.Editor]: 20,
  [Role.Viewer]: 5,
};

const raw: RoleValue = "editor";
console.log(fromKey("Editor"), keyOf(Role.Viewer), raw);
console.log(Object.keys(Role), Object.values(Role));
console.log(isRole("admin"), isRole("owner"));
console.log(limits);
Terminaltsx
~/ts-cheatsheet $ npx tsx enum-keys.ts
editor Viewer editor
[ 'Admin', 'Editor', 'Viewer' ] [ 'admin', 'editor', 'viewer' ]
true false
{ admin: 100, editor: 20, viewer: 5 }
enum-loop.tsTS
enum Level { Low = 1, Mid, High }      // Mid = 2, High = 3

// Numeric enums hold reverse keys too, so filter them out
const names = Object.keys(Level).filter((k) => isNaN(Number(k)));
const values = Object.values(Level).filter((v): v is Level => typeof v === "number");

for (const name of names) {
  console.log(name, Level[name as keyof typeof Level]);
}
console.log(values);
Terminaltsx
~/ts-cheatsheet $ npx tsx enum-loop.ts
Low 1
Mid 2
High 3
[ 1, 2, 3 ]

Why it matters: Object.keys on a numeric enum returns '1', '2', '3' as well as the names. Filter first, or prefer a string enum.

KindSyntaxRuntime objectReverse map
Numericenum A { X, Y }YesYes
Stringenum A { X = "x" }YesNo
Computed membersAll = Read | WriteYesNumeric only
const enumconst enum A { X }No, values are inlinedNo
declare enumdeclare enum A { X }No, it already existsDepends on the source
07

as const objects and dynamic keys

A plain object with as const gives you an enum with no extra code. Then type Object.keys, build reverse maps and assign to keys held in variables.

as constTyped Object.keysReverse mapsRecord

Many codebases prefer a frozen object over an enum: it is plain JavaScript, it tree shakes, and keyof typeof and indexed access pull out both unions. The mapped type with as below flips keys and values, so the reverse map is typed in both directions.

as-const.tsTS
const HttpStatus = {
  Ok: 200,
  NotFound: 404,
  ServerError: 500,
} as const;

type HttpStatusKey = keyof typeof HttpStatus;              // "Ok" | "NotFound" | "ServerError"
type HttpStatusCode = (typeof HttpStatus)[HttpStatusKey];  // 200 | 404 | 500

// Reverse map: code back to its name, typed both ways
const statusName = Object.fromEntries(
  Object.entries(HttpStatus).map(([k, v]) => [v, k]),
) as { [K in HttpStatusKey as (typeof HttpStatus)[K]]: K };

// Object.keys and Object.entries return string by design; these keep the keys
const keys = <T extends object>(o: T) => Object.keys(o) as (keyof T)[];
const entries = <T extends object>(o: T) =>
  Object.entries(o) as { [K in keyof T]: [K, T[K]] }[keyof T][];

function respond(code: HttpStatusCode) {
  return `${code} ${statusName[code]}`;
}

console.log(respond(HttpStatus.NotFound));
console.log(keys(HttpStatus));
console.log(entries(HttpStatus));
Terminaltsx
~/ts-cheatsheet $ npx tsx as-const.ts
404 NotFound
[ 'Ok', 'NotFound', 'ServerError' ]
[ [ 'Ok', 200 ], [ 'NotFound', 404 ], [ 'ServerError', 500 ] ]
dynamic-keys.tsTS
const ROLES = ["admin", "editor", "viewer"] as const;
type Role = (typeof ROLES)[number];      // "admin" | "editor" | "viewer"

// An object built from the list, typed with every key
const labels = Object.fromEntries(ROLES.map((r) => [r, r.toUpperCase()])) as Record<Role, string>;

// Assign to a key held in a variable
const counts: Partial<Record<Role, number>> = {};
function bump(role: Role) {
  counts[role] = (counts[role] ?? 0) + 1;
}
bump("admin"); bump("admin"); bump("viewer");

// Read a property by a key held in a variable
function pluck<T, K extends keyof T>(obj: T, key: K): T[K] {
  return obj[key];
}
const user = { name: "Shree", level: 3 };
const field: keyof typeof user = "level";

console.log(labels);
console.log(counts);
console.log(pluck(user, field), pluck(user, "name"));
Terminaltsx
~/ts-cheatsheet $ npx tsx dynamic-keys.ts
{ admin: 'ADMIN', editor: 'EDITOR', viewer: 'VIEWER' }
{ admin: 2, viewer: 1 }
3 Shree
string-index.tsTS
const theme = { light: "#fff", dark: "#111" };

function pick(name: string) {
  return theme[name];          // rejected: any string could be passed
}

function pickSafe(name: keyof typeof theme) {
  return theme[name];          // fine: only "light" or "dark"
}
Terminaltsc
~/ts-cheatsheet $ npx tsc --noEmit --strict string-index.ts
string-index.ts(4,10): error TS7053: Element implicitly has an 'any' type because expression of type 'string' can't be used to index type '{ light: string; dark: string; }'.
Found 1 error in string-index.ts

Why it matters: when a key really comes from outside, check it first with name in theme and a type guard, then index.

ApproachRuntime codeGet the union of valuesBest for
enumAn object, plus reverse keys if numeric`${E}`Teams that like named members
as const objectThe object you wrote(typeof O)[keyof typeof O]Most new code
String unionNoneIt is already the unionSimple sets with no lookup table
as const arrayThe array(typeof A)[number]Lists you also loop over
08

Classes

Classes with access modifiers, parameter properties, abstract members, getters, static fields and interfaces they promise to implement.

private, protectedabstractimplementsParameter properties

A parameter property like constructor(public width: number) declares and assigns the field in one go. private is checked by the compiler only, while #field is private at runtime too. override makes the compiler confirm the method really exists on the parent.

classes.tsTS
interface Shape { area(): number }

abstract class Base implements Shape {
  static count = 0;
  constructor(protected readonly name: string) { Base.count++; }
  abstract area(): number;
  describe(): string { return `${this.name}: ${this.area()}`; }
}

class Rect extends Base {
  #secret = "private at runtime";   // JavaScript private field
  private cache?: number;           // TypeScript private, compile time only
  constructor(public width: number, public height: number) {
    super("rect");
  }
  area(): number { return (this.cache ??= this.width * this.height); }
  get isSquare(): boolean { return this.width === this.height; }
  override describe(): string { return super.describe().toUpperCase(); }
}

const r = new Rect(3, 4);
console.log(r.area(), r.isSquare, r.describe());
console.log(Base.count, r instanceof Base);
Terminaltsx
~/ts-cheatsheet $ npx tsx classes.ts
12 false RECT: 12
1 true
ModifierVisible fromNote
publicEverywhereThe default
protectedThe class and its subclassesCompile time only
privateThe class itselfCompile time only
#nameThe class itselfEnforced by JavaScript at runtime
readonlyRead anywhere, set in the constructorCombine with any of the above
staticThe class, not instancesBase.count
abstractMust be implemented by a subclassThe class cannot be created directly
09

Generics

A type parameter is a variable for types. It keeps the link between what goes into a function, interface or class and what comes out.

Generic interfacesGeneric classesDefaults

Without generics, first(items) would return any or force one fixed type. With <T>, the caller's type flows through. TypeScript usually infers T from the arguments, so you only write it when inference cannot see it, as with new Stack<number>().

Explicitfirst<string>(["a"])You name T.
Inferredfirst(["a"])T is read from the argument.
generics.tsTS
function first<T>(items: T[]): T | undefined {
  return items[0];
}
const n = first([3, 1, 2]);       // number | undefined
const s = first(["a", "b"]);      // string | undefined

// Generic interface with a default type parameter
interface ApiResponse<TData, TError = string> {
  ok: boolean;
  data?: TData;
  error?: TError;
}
type Pair<A, B> = [A, B];

// Generic class
class Stack<T> {
  private items: T[] = [];
  push(item: T): this { this.items.push(item); return this; }
  pop(): T | undefined { return this.items.pop(); }
  get size(): number { return this.items.length; }
}

const res: ApiResponse<{ id: number }> = { ok: true, data: { id: 7 } };
const pair: Pair<string, number> = ["age", 30];
const stack = new Stack<number>().push(1).push(2);

console.log(n, s, res.data?.id);
console.log(pair, stack.pop(), stack.size);
Terminaltsx
~/ts-cheatsheet $ npx tsx generics.ts
3 a 7
[ 'age', 30 ] 2 1

Why it matters: name type parameters by role when there is more than one, like TData and TError. Single letters are fine for one.

10

Generic constraints

extends limits what a type parameter may be, so you can use its members. K extends keyof T is the most useful line in TypeScript.

T extends UK extends keyof TT[K]const T

A bare T could be anything, so you cannot read .length on it. T extends { length: number } promises that it has one. K extends keyof T ties a key to an object, and the return type T[K] follows whichever key the caller passes.

constraints.tsTS
function longest<T extends { length: number }>(a: T, b: T): T {
  return a.length >= b.length ? a : b;
}

// K must be a key of T, and the return type follows the key
function get<T, K extends keyof T>(obj: T, key: K): T[K] {
  return obj[key];
}

// One parameter constrained by another
function merge<T extends object, U extends Partial<T>>(base: T, patch: U): T & U {
  return { ...base, ...patch };
}

// const type parameter (TS 5.0): literal types without writing `as const`
function routes<const T extends readonly string[]>(paths: T): T {
  return paths;
}

const user = { id: 1, name: "Shree", tags: ["ts"] };
const r = routes(["/home", "/about"]);   // readonly ["/home", "/about"]

console.log(longest("cat", "tiger"), longest([1, 2], [3]));
console.log(get(user, "name"), get(user, "tags"));
console.log(merge(user, { name: "Shri" }));
console.log(r);
Terminaltsx
~/ts-cheatsheet $ npx tsx constraints.ts
tiger [ 1, 2 ]
Shree [ 'ts' ]
{ id: 1, name: 'Shri', tags: [ 'ts' ] }
[ '/home', '/about' ]
constraint-errors.tsTS
function get<T, K extends keyof T>(obj: T, key: K): T[K] {
  return obj[key];
}
function longest<T extends { length: number }>(a: T, b: T): T {
  return a.length >= b.length ? a : b;
}

const user = { id: 1, name: "Shree" };
get(user, "age");
longest(10, 20);
Terminaltsc
~/ts-cheatsheet $ npx tsc --noEmit --strict constraint-errors.ts
constraint-errors.ts(9,11): error TS2345: Argument of type '"age"' is not assignable to parameter of type '"name" | "id"'.
constraint-errors.ts(10,9): error TS2345: Argument of type 'number' is not assignable to parameter of type '{ length: number; }'.
Found 2 errors in constraint-errors.ts

Why it matters: the error names the exact keys that are allowed, so a typo in a key string is caught where you type it.

11

keyof, typeof and indexed access

Build types out of values and other types. typeof reads a value's type, keyof lists its keys, and T[K] reaches inside.

typeof valuekeyof TT[K]T[number]

These three operators let one object be the source of truth. Write the config once, then derive its type, its key union and the type of any nested field. T[number] reads the item type of an array, and a union of keys inside the brackets reads several fields at once.

type-operators.tsTS
const config = {
  port: 3000,
  db: { host: "localhost", pool: 10 },
  features: ["auth", "cache"],
};

type Config = typeof config;                 // the type of a value
type ConfigKey = keyof Config;               // "port" | "db" | "features"
type Db = Config["db"];                      // { host: string; pool: number }
type Host = Config["db"]["host"];            // string
type Feature = Config["features"][number];   // string
type PortOrDb = Config["port" | "db"];       // number | { host: string; pool: number }

function setting<K extends ConfigKey>(key: K): Config[K] {
  return config[key];
}

// typeof a function, then pull pieces out of it
function createUser(name: string, admin = false) {
  return { name, admin, createdAt: 0 };
}
type NewUser = ReturnType<typeof createUser>;     // { name: string; admin: boolean; createdAt: number }
type CreateArgs = Parameters<typeof createUser>;  // [name: string, admin?: boolean]

const u: NewUser = createUser("Shree");
const args: CreateArgs = ["Ana", true];

console.log(setting("port"), setting("db").host);
console.log(Object.keys(config) as ConfigKey[]);
console.log(u, args);
Terminaltsx
~/ts-cheatsheet $ npx tsx type-operators.ts
3000 localhost
[ 'port', 'db', 'features' ]
{ name: 'Shree', admin: false, createdAt: 0 } [ 'Ana', true ]
OperatorReadsExampleResult
typeofThe type of a valuetypeof configThe object's shape
keyofThe keys of a typekeyof Config"port" | "db" | "features"
T[K]One field's typeConfig["port"]number
T[A | B]Several fieldsConfig["port" | "db"]A union of both
T[number]Array item typestring[][number]string
T[keyof T]Every value typeConfig[keyof Config]A union of all fields
12

Mapped types

[K in keyof T] loops over keys and builds a new type. Add or strip modifiers, rename keys with as, and drop keys by mapping them to never.

[K in keyof T]+readonly, -?Key remappingas never

Read { [K in keyof T]: T[K] } as a for loop over the keys of T that copies each value type. Change the right side to change every value, put -readonly or -? in front of the key to remove a modifier, and add as after it to rename or filter keys.

Before{ id: number }User
After Nullable{ id: number | null }Every value widened.
mapped.tsTS
type User = { id: number; name: string; email?: string };

// Visit every key K of T and decide its value type
type Nullable<T> = { [K in keyof T]: T[K] | null };

// Add or remove modifiers with + and -
type Mutable<T> = { -readonly [K in keyof T]: T[K] };
type Concrete<T> = { [K in keyof T]-?: T[K] };
type Frozen<T> = { +readonly [K in keyof T]: T[K] };

// Remap keys with `as`: rename them, or drop one by mapping it to never
type Getters<T> = {
  [K in keyof T as `get${Capitalize<string & K>}`]: () => T[K];
};
type WithoutId<T> = { [K in keyof T as K extends "id" ? never : K]: T[K] };

// Map over any union of strings, not only keyof
type Flags = { [K in "dark" | "beta"]: boolean };

function makeGetters<T extends object>(obj: T): Getters<T> {
  const out: Record<string, () => unknown> = {};
  for (const key of Object.keys(obj) as (keyof T & string)[]) {
    out[`get${key[0].toUpperCase()}${key.slice(1)}`] = () => obj[key];
  }
  return out as Getters<T>;
}

const g = makeGetters({ id: 1, name: "Shree" });
const patch: WithoutId<User> = { name: "Shri" };
const flags: Flags = { dark: true, beta: false };
const draft: Nullable<User> = { id: null, name: "Shree" };

console.log(g.getId(), g.getName());
console.log(patch, flags);
console.log(draft);
Terminaltsx
~/ts-cheatsheet $ npx tsx mapped.ts
1 Shree
{ name: 'Shri' } { dark: true, beta: false }
{ id: null, name: 'Shree' }

Why it matters: Partial, Required, Readonly, Pick and Record are all mapped types like these, written in the standard library.

PatternDoes
{ [K in keyof T]: X }Replaces every value type with X
{ readonly [K in keyof T]: T[K] }Adds readonly to every key
{ -readonly [K in keyof T]: T[K] }Removes readonly
{ [K in keyof T]-?: T[K] }Makes every key required
{ [K in keyof T as NewKey]: T[K] }Renames keys
{ [K in keyof T as K extends X ? never : K]: T[K] }Filters keys out
13

Conditional types and infer

T extends U ? X : Y chooses a type the way a ternary chooses a value. infer captures a piece of the matched type for you to reuse.

extends ? :DistributioninferRecursion

When T is a naked type parameter and you pass a union, the condition runs once per member and the results are joined again. That is called distribution. Wrap both sides in a tuple, [T] extends [U], to check the union as a whole. infer only works inside the extends clause and names whatever sits in that position.

conditional.tsTS
type IsString<T> = T extends string ? true : false;
type A = IsString<"hi">;                     // true
type B = IsString<42>;                       // false

// Distribution over a union, and how to switch it off
type ToArray<T> = T extends unknown ? T[] : never;
type ToArrayAll<T> = [T] extends [unknown] ? T[] : never;
type C = ToArray<string | number>;           // string[] | number[]
type D = ToArrayAll<string | number>;        // (string | number)[]

// infer captures a piece of the matched type
type ElementOf<T> = T extends readonly (infer E)[] ? E : T;
type Unwrap<T> = T extends Promise<infer V> ? Unwrap<V> : T;
type FirstArg<F> = F extends (first: infer P, ...rest: any[]) => any ? P : never;
type RouteParam<S> = S extends `${string}:${infer P}/${infer Rest}`
  ? P | RouteParam<`/${Rest}`>
  : S extends `${string}:${infer P}` ? P : never;

type E = ElementOf<number[]>;                          // number
type F = Unwrap<Promise<Promise<string>>>;             // string
type G = FirstArg<(id: number, name: string) => void>; // number
type R = RouteParam<"/users/:userId/posts/:postId">;   // "userId" | "postId"

// At runtime: params must have exactly the keys found in the path
function buildPath<P extends string>(path: P, params: Record<RouteParam<P>, string>): string {
  return path.replace(/:(\w+)/g, (_, k: string) => (params as Record<string, string>)[k]);
}

console.log(buildPath("/users/:userId/posts/:postId", { userId: "42", postId: "7" }));
console.log(buildPath("/teams/:teamId", { teamId: "backend" }));
Terminaltsx
~/ts-cheatsheet $ npx tsx conditional.ts
/users/42/posts/7
/teams/backend

Why it matters: the route example shows type level code paying off at runtime: forget a param or misspell one and the call no longer compiles.

14

Template literal types

Backtick strings at the type level. Combine unions into every pattern, change case with four built in helpers, and pick strings apart with infer.

`a-${B}`CapitalizePattern typesString parsing

A template literal type with unions inside expands to every combination, so `btn-${Size}-${Color}` with three sizes and two colours makes six string types. With ${number} or ${string} inside, it becomes a pattern that any matching string satisfies.

template-types.tsTS
type Size = "sm" | "md" | "lg";
type Color = "red" | "blue";
type ClassName = `btn-${Size}-${Color}`;    // 6 combinations

type EventName<T extends string> = `on${Capitalize<T>}`;
type Handlers = { [E in "click" | "focus" as EventName<E>]: () => void };
// { onClick: () => void; onFocus: () => void }

// The four built in string helpers
type Loud = Uppercase<"get">;         // "GET"
type Quiet = Lowercase<"POST">;       // "post"
type Cap = Capitalize<"user">;        // "User"
type Uncap = Uncapitalize<"Item">;    // "item"

// Pattern types: any string with this shape
type Px = `${number}px`;
type ApiPath = `/api/${string}`;

// Split a dotted path into a tuple, one segment at a time
type Split<S extends string, D extends string> =
  S extends `${infer Head}${D}${infer Tail}` ? [Head, ...Split<Tail, D>] : [S];
type Parts = Split<"a.b.c", ".">;     // ["a", "b", "c"]

const cls: ClassName = "btn-md-blue";
const width: Px = "240px";
const path: ApiPath = "/api/users";
const parts: Parts = ["a", "b", "c"];
const handlers: Handlers = { onClick: () => console.log("clicked"), onFocus: () => {} };

handlers.onClick();
console.log(cls, width, path, parts);
Terminaltsx
~/ts-cheatsheet $ npx tsx template-types.ts
clicked
btn-md-blue 240px /api/users [ 'a', 'b', 'c' ]
15

Utility types for objects

Partial, Required, Readonly, Pick, Omit and Record reshape one model into the many variants an API needs.

PartialPick and OmitRecordReadonly
Partial<T>

Every key becomes optional. Good for patch bodies.

See the example
Optional
Required<T>

Every key becomes required, even ones marked ?.

See the example
Strict
Readonly<T>

Every key becomes readonly at compile time.

See the example
Frozen
Pick<T, K>

Keeps only the keys you list.

See the example
Keep keys
Omit<T, K>

Drops the keys you list.

See the example
Drop keys
Record<K, V>

An object with keys K, each holding a V.

See the example
Lookup
object-utils.tsTS
interface User {
  id: number;
  name: string;
  email: string;
  password: string;
  bio?: string;
}

type UserPatch = Partial<User>;                      // every key optional
type FullUser = Required<User>;                      // bio required too
type FrozenUser = Readonly<User>;                    // every key readonly
type PublicUser = Omit<User, "password">;            // drop keys
type Credentials = Pick<User, "email" | "password">; // keep keys
type UsersById = Record<number, PublicUser>;         // keys and one value type
type UpdateBody = Partial<Omit<User, "id">> & Pick<User, "id">;  // combine them

function update(user: User, patch: UserPatch): User {
  return { ...user, ...patch };
}
function toPublic({ password, ...rest }: User): PublicUser {
  return rest;
}

const user: User = { id: 1, name: "Shree", email: "s@x.dev", password: "hunter2" };
const updated = update(user, { name: "Shri" });
const directory: UsersById = { [user.id]: toPublic(updated) };
const body: UpdateBody = { id: 1, bio: "backend" };

console.log(updated.name, Object.keys(toPublic(user)));
console.log(directory);
console.log(body);
Terminaltsx
~/ts-cheatsheet $ npx tsx object-utils.ts
Shri [ 'id', 'name', 'email' ]
{ '1': { id: 1, name: 'Shri', email: 's@x.dev' } }
{ id: 1, bio: 'backend' }

Why it matters: Omit removes the type, not the value. The destructuring in toPublic is what actually leaves the password out of the response.

16

Utility types for unions and functions

Exclude and Extract filter unions. ReturnType, Parameters, Awaited and friends read types off functions, classes and promises.

Exclude, ExtractReturnTypeAwaitedNoInfer

These helpers mean you never copy a function's types by hand. Ask for Parameters<typeof fn> or Awaited<ReturnType<typeof fn>> and the derived type changes when the function does. NoInfer (TS 5.4) stops a parameter from widening a type parameter.

function-utils.tsTS
type Status = "idle" | "loading" | "success" | "error";

type Active = Exclude<Status, "idle">;             // "loading" | "success" | "error"
type Done = Extract<Status, "success" | "error">;  // "success" | "error"
type Sure = NonNullable<string | null | undefined>; // string

async function fetchUser(id: number, withPosts = false) {
  return { id, name: "Shree", posts: withPosts ? 3 : 0 };
}

type Args = Parameters<typeof fetchUser>;     // [id: number, withPosts?: boolean]
type Result = ReturnType<typeof fetchUser>;   // Promise<{ id: number; name: string; posts: number }>
type UserData = Awaited<Result>;              // { id: number; name: string; posts: number }

class Logger { constructor(public prefix: string) {} }
type LoggerArgs = ConstructorParameters<typeof Logger>;  // [prefix: string]
type LoggerInstance = InstanceType<typeof Logger>;       // Logger

// NoInfer: only `options` decides T, so `fallback` must be one of them
function pickDefault<T extends string>(options: T[], fallback: NoInfer<T>): T {
  return options.includes(fallback) ? fallback : options[0];
}

async function main() {
  const args: Args = [7, true];
  const data: UserData = await fetchUser(...args);
  const loggerArgs: LoggerArgs = ["[api]"];
  const log: LoggerInstance = new Logger(...loggerArgs);
  const ready: Sure = "ready";
  console.log(log.prefix, data);
  console.log(pickDefault(["sm", "md"], "md"), ready);
}
main();
Terminaltsx
~/ts-cheatsheet $ npx tsx function-utils.ts
[api] { id: 7, name: 'Shree', posts: 3 }
md ready
no-infer.tsTS
function pickDefault<T extends string>(options: T[], fallback: NoInfer<T>): T {
  return options.includes(fallback) ? fallback : options[0];
}

pickDefault(["sm", "md"], "lg");
Terminaltsc
~/ts-cheatsheet $ npx tsc --noEmit --strict no-infer.ts
no-infer.ts(5,27): error TS2345: Argument of type '"lg"' is not assignable to parameter of type '"sm" | "md"'.
Found 1 error in no-infer.ts

Why it matters: without NoInfer, "lg" would quietly widen T to all three sizes and the mistake would compile.

UtilityInputGives back
Exclude<U, X>A unionThe members not assignable to X
Extract<U, X>A unionThe members assignable to X
NonNullable<T>Any typeT without null and undefined
Parameters<F>A function typeIts parameters as a tuple
ReturnType<F>A function typeWhat it returns
Awaited<T>A promise, nested or notThe resolved value
ConstructorParameters<C>A classThe constructor's parameters
InstanceType<C>A classThe type of its instances
NoInfer<T>A type parameter useThe same T, but not an inference site
17

Type guards, assertions and satisfies

Write your own narrowing with value is T and asserts. Know when as, !, as const and satisfies help, and when they hide bugs.

v is Tassertsas and !satisfies

A function that returns pet is Cat teaches the compiler a new narrowing check, and it works inside filter too. An asserts function narrows everything after the call or throws. as and ! skip checks entirely, so keep them for cases you can prove. satisfies checks a value against a type while keeping its precise inferred type.

guards.tsTS
type Cat = { kind: "cat"; meow(): string };
type Dog = { kind: "dog"; bark(): string };

// A type guard returns `value is Type`
function isCat(pet: Cat | Dog): pet is Cat {
  return pet.kind === "cat";
}

// An assertion function narrows everything after the call
function assertDefined<T>(value: T, msg: string): asserts value is NonNullable<T> {
  if (value === null || value === undefined) throw new Error(msg);
}

// satisfies checks the shape and keeps the precise type of each key
type Theme = Record<string, string | [number, number, number]>;
const palette = {
  coral: "#FF4D6D",
  violet: [131, 56, 236],
} satisfies Theme;

const pets: (Cat | Dog)[] = [
  { kind: "cat", meow: () => "meow" },
  { kind: "dog", bark: () => "woof" },
];

const cats = pets.filter(isCat);                       // Cat[]
const found = pets.find(isCat)!;                       // ! says "not undefined"
const raw = JSON.parse('{"n": 1}') as { n: number };  // as says "trust me"
const el = pets.length ? { value: "42" } : null;
assertDefined(el, "missing element");

console.log(cats.map((c) => c.meow()), found.kind);
console.log(palette.coral.toLowerCase(), palette.violet.join(","));
console.log(el.value, raw.n);
Terminaltsx
~/ts-cheatsheet $ npx tsx guards.ts
[ 'meow' ] cat
#ff4d6d 131,56,236
42 1
satisfies.tsTS
type Theme = Record<string, string | [number, number, number]>;

const annotated: Theme = { coral: "#FF4D6D", violet: [131, 56, 236] };
annotated.coral.toLowerCase();      // rejected: it might be a tuple

const checked = { coral: "#FF4D6D", violet: [131, 56, 236] } satisfies Theme;
checked.coral.toLowerCase();        // fine: coral is known to be a string
Terminaltsc
~/ts-cheatsheet $ npx tsc --noEmit --strict satisfies.ts
satisfies.ts(4,17): error TS2339: Property 'toLowerCase' does not exist on type 'string | [number, number, number]'.
Found 1 error in satisfies.ts

Why it matters: an annotation replaces the inferred type with the wider one. satisfies validates without throwing that detail away.

ToolWhat the compiler doesRisk
v is TNarrows where the guard returns trueA wrong body lies to the compiler
asserts v is TNarrows after the callMust throw when the check fails
x as TTreats x as TNo runtime check at all
x!Removes null and undefinedCrashes if you were wrong
as constKeeps literals, adds readonlyNone
x satisfies TChecks x against T, keeps x's typeNone
18

Advanced patterns

Patterns that turn up in real backends: branded ids, deep recursive types, variadic tuples, a Result type and exhaustive lookups.

Branded typesDeepPartialVariadic tuplesResult

TypeScript compares types by shape, so two string ids are interchangeable. A brand adds a property that never exists at runtime, which keeps a UserId from being passed where an OrderId is expected. Recursive types call themselves to reach every level of a nested object.

patterns.tsTS
// Branded types: two strings the compiler keeps apart
type Brand<T, B extends string> = T & { readonly __brand: B };
type UserId = Brand<string, "UserId">;
type OrderId = Brand<string, "OrderId">;
const userId = (s: string) => s as UserId;
function loadUser(id: UserId) { return `user ${id}`; }

// Recursive types
type DeepPartial<T> = { [K in keyof T]?: T[K] extends object ? DeepPartial<T[K]> : T[K] };
type DeepReadonly<T> = { readonly [K in keyof T]: T[K] extends object ? DeepReadonly<T[K]> : T[K] };
type Json = string | number | boolean | null | Json[] | { [key: string]: Json };

// Variadic tuples
type Prepend<H, T extends unknown[]> = [H, ...T];
function tail<T extends unknown[]>([, ...rest]: [unknown, ...T]): T { return rest; }

// A Result type instead of throwing
type Result<T, E = string> = { ok: true; value: T } | { ok: false; error: E };
function safeDivide(a: number, b: number): Result<number> {
  return b === 0 ? { ok: false, error: "divide by zero" } : { ok: true, value: a / b };
}

// An exhaustive lookup: every member of the union must be present
type Env = "dev" | "prod";
const urls = { dev: "localhost", prod: "api.example.com" } satisfies Record<Env, string>;

type Settings = { db: { host: string; port: number }; debug: boolean };
const overrides: DeepPartial<Settings> = { db: { port: 5433 } };
const payload: Json = { ids: [1, 2], meta: { draft: true } };
const args: Prepend<string, [number, boolean]> = ["x", 1, true];

console.log(loadUser(userId("u_42")));
console.log(overrides, payload);
console.log(tail(args), safeDivide(10, 4), safeDivide(1, 0));
console.log(urls.prod);
Terminaltsx
~/ts-cheatsheet $ npx tsx patterns.ts
user u_42
{ db: { port: 5433 } } { ids: [ 1, 2 ], meta: { draft: true } }
[ 1, true ] { ok: true, value: 2.5 } { ok: false, error: 'divide by zero' }
api.example.com
brand-error.tsTS
type UserId = string & { readonly __brand: "UserId" };
function loadUser(id: UserId) { return id; }

loadUser("u_42");                 // rejected: a plain string is not a UserId
loadUser("u_42" as UserId);       // fine: branded at the boundary
Terminaltsc
~/ts-cheatsheet $ npx tsc --noEmit --strict brand-error.ts
brand-error.ts(4,10): error TS2345: Argument of type 'string' is not assignable to parameter of type 'UserId'.
Found 1 error in brand-error.ts

Why it matters: brand ids once where they enter the system, such as after validating a request, and the rest of the code cannot mix them up.

19

Modules, declarations and tsconfig

Import and export types, describe untyped code with declare and .d.ts files, and switch on the compiler flags that catch the most bugs.

import typedeclare.d.tstsconfig

import type is erased completely at build time, so it can never pull a module into your bundle or create a runtime cycle. With verbatimModuleSyntax on, TypeScript requires it for every type only import.

api.tsTS
// types.ts
export interface User { id: number; name: string }
export type Role = "admin" | "viewer";
export default class Client {}

// api.ts
import Client, { type User } from "./types";   // inline type only import
import type { Role } from "./types";            // the whole line is erased
export type { User };                            // re-export a type

A .d.ts file holds only types. Use declare for things that exist at runtime but that TypeScript cannot see, such as a value injected by your bundler, a file type you import, or a field you add to a library's interface.

global.d.tsTS
// A constant injected at build time
declare const __APP_VERSION__: string;

// Importing a file type
declare module "*.svg" {
  const src: string;
  export default src;
}

// Module augmentation: add a field to Express's Request
declare module "express-serve-static-core" {
  interface Request { userId?: string }
}

// Add to a global interface
declare global {
  interface Window { analytics?: { track(event: string): void } }
}
export {};
tsconfig.jsonJSON
{
  "compilerOptions": {
    "target": "ES2022",
    "module": "NodeNext",
    "moduleResolution": "NodeNext",
    "strict": true,
    "noUncheckedIndexedAccess": true,
    "exactOptionalPropertyTypes": true,
    "noImplicitOverride": true,
    "verbatimModuleSyntax": true,
    "skipLibCheck": true,
    "declaration": true,
    "outDir": "dist"
  },
  "include": ["src"]
}
Terminalworkflow
~/ts-cheatsheet $ npm i -D typescript tsx
# the compiler, and a fast runner for .ts files
~/ts-cheatsheet $ npx tsc --init
# writes a starter tsconfig.json
~/ts-cheatsheet $ npx tsc --noEmit
# type check the whole project, write nothing
~/ts-cheatsheet $ npx tsx src/index.ts
# run a file directly, no build step
~/ts-cheatsheet $ npx tsc
# build to dist/ with .d.ts files
FlagCatches
strictTurns on the whole strict family, including strictNullChecks and noImplicitAny
noUncheckedIndexedAccessarr[i] and obj[key] may be undefined
exactOptionalPropertyTypesSetting an optional field to undefined on purpose
noImplicitOverrideOverriding a method without the override keyword
verbatimModuleSyntaxType imports written as value imports
skipLibCheckNothing; it skips checking .d.ts files for speed
20

Which one do I need?

Start from what you are trying to do, then reach for the tool in the middle column. The last column takes you to the module that explains it.

I want toReach forExampleModule
Describe an object shapeinterface or typeinterface User { id: number }04
Allow one of a few valuesUnion of literals"sm" | "md" | "lg"02
Combine two shapesIntersectionUser & Timestamps03
Named constants at runtimeenum or as const objectconst S = { Ok: 200 } as const07
The union of an object's keyskeyof typeofkeyof typeof Role06
The union of an array's itemsIndexed access with number(typeof ROLES)[number]07
Index an object with a variableK extends keyof Tget<T, K extends keyof T>(o: T, k: K)10
Keep input and output linkedGenericfunction first<T>(xs: T[]): T09
Make every key optionalPartialPartial<User>15
Keep or drop some keysPick or OmitOmit<User, "password">15
Object keyed by a fixed setRecordRecord<Role, number>15
Transform every keyMapped type{ [K in keyof T]: T[K] | null }12
Choose a type by a conditionConditional typeT extends string ? A : B13
Pull out an inner typeinferT extends Promise<infer V> ? V : T13
Build string patternsTemplate literal type`on${Capitalize<E>}`14
Reuse a function's typesParameters, ReturnType, AwaitedAwaited<ReturnType<typeof f>>16
Narrow with your own checkType guard(p): p is Cat =>17
Check a shape, keep literalssatisfies{ ... } satisfies Theme17
Stop two ids from mixingBranded typestring & { __brand: "UserId" }18
Handle every union membernever checkconst x: never = value03