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.
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
- Think of it as a post office that checks every parcel's label before it ships
- 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
- Think of it as the label on a parcel saying what's inside
- 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,numberandbooleanare the simplest types. - Compiler, compile time and runtime
- Think of it as the clerk who inspects labels at the counter, before the parcel is out on the road
- 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
- Think of it as writing the label on the parcel yourself
- 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
- Think of it as one plain item, like a single letter, rather than a box of things
- The simplest kinds of value:
string(text),number,boolean(true or false),bigint,symbol,nullandundefined. 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
- Think of it as a sack of letters, all of the same kind
- 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
- Think of it as a row of pigeonholes, each marked for one particular thing
- 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
- Think of it as a parcel with no label, one held for inspection, and one that can't exist
anyswitches checking off for a value.unknownalso accepts anything, but makes you check what it is before you use it.neveris the type of something that can't happen, such as the result of a function that always throws an error.- Type inference
- Think of it as the clerk looking inside and writing the label for you
- 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 = 10is already known to be anumber. - Literal type
- Think of it as a label naming one exact item, not just a kind of item
- A type that allows a single exact value, like
"dark"or3.constkeeps that exact value as the type, whileletwidens it to the general one (string,number) because it might change later. - as const
- Think of it as sealing a parcel so nothing inside can be swapped
- 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
- Think of it as a label reading "letter or small parcel"
- 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
- Think of it as a parcel that has to meet two sets of rules at once
- Written with
&, it joins two types into one that has everything from both. - For example
User & Timestampshas a name and a creation date. - Narrowing
- Think of it as opening the parcel to see whether it's the letter or the box
- Checking a value inside an
iforswitchso that, in that branch, TypeScript knows its exact type.typeof,inandinstanceofchecks all narrow. - Discriminated union
- Think of it as parcels that each carry a coloured sticker saying which kind they are
- 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
- Think of it as a standard form listing what a parcel must contain
- 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
- Think of it as a nickname you can give to any label
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
- Think of it as a box on the form you may leave blank, and one printed in ink
- A
?after a field name means it can be missing.readonlymeans it is set once and can't be changed afterwards. Both are checked by the compiler only. - For example
email?: stringandreadonly id: number - Enum
- Think of it as the fixed list of stamp codes the office accepts
- 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
- Think of it as one counter with a separate sign for letters and for parcels
- 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
- Think of it as the master design every new parcel box is cut from
- A blueprint for making objects that share the same fields and methods. Words like
public,protectedandprivatesay who may touch each member, and TypeScript checks them before the code runs. - For example
new Rect(3, 4)builds one object from theRectclass.
Types that build types
Labels with a blank in them, and tools that read one label to write another.
- Generic
- Think of it as a box with a slot where the sender writes what's inside
- 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[]): Thands back a number from a list of numbers. - Generic constraint
- Think of it as a slot that only takes parcels under 2 kg
T extends Xlimits 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
- Think of it as the list of box names on a form, and copying the label off a parcel already packed
keyofgives 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
- Think of it as pointing at one box on the form and asking what goes in it
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 thedbfield. - Mapped type
- Think of it as photocopying a form and changing every line the same way
- 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
- Think of it as a sorting rule: letters to this bin, everything else to that one
T extends U ? X : Ypicks one type or another depending on whether T fits U, the way? :picks a value. Inside it,infercaptures a piece of the matched type, such as the value a promise holds.- Template literal type
- Think of it as a label printed from a pattern, like "btn, size, colour"
- 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}pxaccepts"240px"but not"wide". - Utility types
- Think of it as ready-made rubber stamps for the changes you make most often
- Generics that come with TypeScript and reshape a type for you.
Partialmakes every field optional,PickandOmitkeep or drop fields,Recordbuilds an object type,ReturnTypereads 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
- Think of it as a scanner you build that tells the clerk what's inside
- A function that returns
value is Cat. When it says true, TypeScript narrows the value to that type, even insidefilter. An assertion function (asserts) does the same job but throws an error when the check fails. - satisfies
- Think of it as the clerk checking a label without rewriting it
value satisfies Typechecks a value against a type but keeps the value's own, more precise type. Compareas, which only tells the checker "trust me" and checks nothing.- For example
const palette = { coral: "#FF4D6D" } satisfies Theme - Branded type
- Think of it as a coloured tag that keeps two identical looking envelopes apart
- A string or number marked with a tag that exists only for the checker. A
UserIdand anOrderIdare 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
- Think of it as sending a labelled parcel from one branch office to another
- Each file is a module that shares things with
exportand receives them withimport.import typebrings in types only and disappears completely when the code compiles. - Declaration file (.d.ts)
- Think of it as a customs form describing a sealed parcel from abroad
- A file that holds only types, describing code TypeScript can't see for itself, such as a plain JavaScript library. The
declarekeyword says something exists without creating it. - For example
declare const __APP_VERSION__: string; - tsconfig
- Think of it as the post office rulebook
tsconfig.jsonis the compiler's settings file: which JavaScript version to write out, how to find modules, and how strict to be."strict": trueturns on the checks that catch the most bugs.
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.
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.
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);~/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.
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);
}~/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.
| Type | What it holds | Use it for |
|---|---|---|
any | Anything, with no checks | A last resort while migrating JavaScript |
unknown | Anything, checks required | Parsed JSON, errors, untrusted input |
never | Nothing at all | Functions that throw, exhaustive switches |
void | No useful return value | Callbacks and functions that only do work |
null / undefined | An empty value | Optional data, with strictNullChecks on |
object | Any non primitive | Rarely; prefer a real shape |
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.
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 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);~/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.
type Align = "left" | "center" | "right";
let align: Align = "left";
align = "middle";~/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.
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.
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.
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 }));~/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.
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"));~/ts-cheatsheet $ npx tsx intersection.ts Shree 0 flap Error: boom plain
| Check | Narrows by | Example |
|---|---|---|
typeof | Primitive type | typeof x === "string" |
in | A property exists | "swim" in pet |
instanceof | Class or constructor | e instanceof Error |
| Literal field | A shared discriminant | s.kind === "circle" |
Array.isArray | Arrays | Array.isArray(v) |
| Truthiness | Removes null and undefined | if (user) { ... } |
never | Exhaustiveness | const x: never = s |
Interfaces and type aliases
Both name an object shape. Interfaces extend and merge; type aliases also name unions, tuples and computed types.
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.
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);~/ts-cheatsheet $ npx tsx shapes.ts Shree [ 'deploy' ] backend { x: 1, y: 2, z: 3 } done
interface User {
readonly id: number;
name: string;
}
const user: User = { id: 1, name: "Shree" };
user.name = "Shri"; // fine
user.id = 2; // rejected~/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.
| Feature | interface | type |
|---|---|---|
| Object shapes | Yes | Yes |
| Unions and tuples | No | Yes |
| Extend another shape | extends | & (intersection) |
| Declaration merging | Yes | No |
| Mapped and conditional types | No | Yes |
| A class can implement it | Yes | Yes, 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.
Functions
Type the parameters and the compiler checks every call. Add optional, default and rest parameters, overloads and a typed this.
Several call signatures over one implementation.
See the exampleA fake first parameter that types this inside the body.
See the example// 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());~/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.
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.
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.
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);~/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 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);~/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 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);~/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.
| Kind | Syntax | Runtime object | Reverse map |
|---|---|---|---|
| Numeric | enum A { X, Y } | Yes | Yes |
| String | enum A { X = "x" } | Yes | No |
| Computed members | All = Read | Write | Yes | Numeric only |
const enum | const enum A { X } | No, values are inlined | No |
declare enum | declare enum A { X } | No, it already exists | Depends on the source |
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.
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.
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));~/ts-cheatsheet $ npx tsx as-const.ts 404 NotFound [ 'Ok', 'NotFound', 'ServerError' ] [ [ 'Ok', 200 ], [ 'NotFound', 404 ], [ 'ServerError', 500 ] ]
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"));~/ts-cheatsheet $ npx tsx dynamic-keys.ts { admin: 'ADMIN', editor: 'EDITOR', viewer: 'VIEWER' } { admin: 2, viewer: 1 } 3 Shree
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"
}~/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.
| Approach | Runtime code | Get the union of values | Best for |
|---|---|---|---|
enum | An object, plus reverse keys if numeric | `${E}` | Teams that like named members |
as const object | The object you wrote | (typeof O)[keyof typeof O] | Most new code |
| String union | None | It is already the union | Simple sets with no lookup table |
as const array | The array | (typeof A)[number] | Lists you also loop over |
Classes
Classes with access modifiers, parameter properties, abstract members, getters, static fields and interfaces they promise to implement.
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.
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);~/ts-cheatsheet $ npx tsx classes.ts 12 false RECT: 12 1 true
| Modifier | Visible from | Note |
|---|---|---|
public | Everywhere | The default |
protected | The class and its subclasses | Compile time only |
private | The class itself | Compile time only |
#name | The class itself | Enforced by JavaScript at runtime |
readonly | Read anywhere, set in the constructor | Combine with any of the above |
static | The class, not instances | Base.count |
abstract | Must be implemented by a subclass | The class cannot be created directly |
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.
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>().
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);~/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.
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.
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.
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);~/ts-cheatsheet $ npx tsx constraints.ts tiger [ 1, 2 ] Shree [ 'ts' ] { id: 1, name: 'Shri', tags: [ 'ts' ] } [ '/home', '/about' ]
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);~/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.
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.
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.
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);~/ts-cheatsheet $ npx tsx type-operators.ts 3000 localhost [ 'port', 'db', 'features' ] { name: 'Shree', admin: false, createdAt: 0 } [ 'Ana', true ]
| Operator | Reads | Example | Result |
|---|---|---|---|
typeof | The type of a value | typeof config | The object's shape |
keyof | The keys of a type | keyof Config | "port" | "db" | "features" |
T[K] | One field's type | Config["port"] | number |
T[A | B] | Several fields | Config["port" | "db"] | A union of both |
T[number] | Array item type | string[][number] | string |
T[keyof T] | Every value type | Config[keyof Config] | A union of all fields |
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.
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.
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);~/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.
| Pattern | Does |
|---|---|
{ [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 |
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.
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.
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" }));~/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.
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 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.
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);~/ts-cheatsheet $ npx tsx template-types.ts clicked btn-md-blue 240px /api/users [ 'a', 'b', 'c' ]
Utility types for objects
Partial, Required, Readonly, Pick, Omit and Record reshape one model into the many variants an API needs.
Every key becomes required, even ones marked ?.
See the exampleKeeps only the keys you list.
See the exampleDrops the keys you list.
See the exampleAn object with keys K, each holding a V.
See the exampleinterface 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);~/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.
Utility types for unions and functions
Exclude and Extract filter unions. ReturnType, Parameters, Awaited and friends read types off functions, classes and promises.
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.
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();~/ts-cheatsheet $ npx tsx function-utils.ts [api] { id: 7, name: 'Shree', posts: 3 } md ready
function pickDefault<T extends string>(options: T[], fallback: NoInfer<T>): T {
return options.includes(fallback) ? fallback : options[0];
}
pickDefault(["sm", "md"], "lg");~/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.
| Utility | Input | Gives back |
|---|---|---|
Exclude<U, X> | A union | The members not assignable to X |
Extract<U, X> | A union | The members assignable to X |
NonNullable<T> | Any type | T without null and undefined |
Parameters<F> | A function type | Its parameters as a tuple |
ReturnType<F> | A function type | What it returns |
Awaited<T> | A promise, nested or not | The resolved value |
ConstructorParameters<C> | A class | The constructor's parameters |
InstanceType<C> | A class | The type of its instances |
NoInfer<T> | A type parameter use | The same T, but not an inference site |
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.
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.
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);~/ts-cheatsheet $ npx tsx guards.ts [ 'meow' ] cat #ff4d6d 131,56,236 42 1
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~/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.
| Tool | What the compiler does | Risk |
|---|---|---|
v is T | Narrows where the guard returns true | A wrong body lies to the compiler |
asserts v is T | Narrows after the call | Must throw when the check fails |
x as T | Treats x as T | No runtime check at all |
x! | Removes null and undefined | Crashes if you were wrong |
as const | Keeps literals, adds readonly | None |
x satisfies T | Checks x against T, keeps x's type | None |
Advanced patterns
Patterns that turn up in real backends: branded ids, deep recursive types, variadic tuples, a Result type and exhaustive lookups.
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.
// 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);~/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
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~/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.
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 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.
// 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 typeA .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.
// 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 {};{
"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"]
}~/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
| Flag | Catches |
|---|---|
strict | Turns on the whole strict family, including strictNullChecks and noImplicitAny |
noUncheckedIndexedAccess | arr[i] and obj[key] may be undefined |
exactOptionalPropertyTypes | Setting an optional field to undefined on purpose |
noImplicitOverride | Overriding a method without the override keyword |
verbatimModuleSyntax | Type imports written as value imports |
skipLibCheck | Nothing; it skips checking .d.ts files for speed |
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 to | Reach for | Example | Module |
|---|---|---|---|
| Describe an object shape | interface or type | interface User { id: number } | 04 |
| Allow one of a few values | Union of literals | "sm" | "md" | "lg" | 02 |
| Combine two shapes | Intersection | User & Timestamps | 03 |
| Named constants at runtime | enum or as const object | const S = { Ok: 200 } as const | 07 |
| The union of an object's keys | keyof typeof | keyof typeof Role | 06 |
| The union of an array's items | Indexed access with number | (typeof ROLES)[number] | 07 |
| Index an object with a variable | K extends keyof T | get<T, K extends keyof T>(o: T, k: K) | 10 |
| Keep input and output linked | Generic | function first<T>(xs: T[]): T | 09 |
| Make every key optional | Partial | Partial<User> | 15 |
| Keep or drop some keys | Pick or Omit | Omit<User, "password"> | 15 |
| Object keyed by a fixed set | Record | Record<Role, number> | 15 |
| Transform every key | Mapped type | { [K in keyof T]: T[K] | null } | 12 |
| Choose a type by a condition | Conditional type | T extends string ? A : B | 13 |
| Pull out an inner type | infer | T extends Promise<infer V> ? V : T | 13 |
| Build string patterns | Template literal type | `on${Capitalize<E>}` | 14 |
| Reuse a function's types | Parameters, ReturnType, Awaited | Awaited<ReturnType<typeof f>> | 16 |
| Narrow with your own check | Type guard | (p): p is Cat => | 17 |
| Check a shape, keep literals | satisfies | { ... } satisfies Theme | 17 |
| Stop two ids from mixing | Branded type | string & { __brand: "UserId" } | 18 |
| Handle every union member | never check | const x: never = value | 03 |