TypeScript Cheatsheet
Unions and Narrowing
Use this TypeScript reference while you build software engineering projects, review code for technical interview prep, or polish examples for a software engineer resume.
Union Types
// Value can be one of several types let id: string | number; id = "abc"; // OK id = 123; // OK // Union with null / undefined type MaybeString = string | null; type Optional<T> = T | undefined; type Nullable<T> = T | null | undefined; // Union in function parameters function format(val: string | number | boolean): string { return String(val); } // Array of union type const items: (string | number)[] = [1, "two", 3];
typeof Narrowing
function process(val: string | number | boolean) { if (typeof val === "string") { val.toUpperCase(); // string } else if (typeof val === "number") { val.toFixed(2); // number } else { val; // boolean } } // All typeof string values // "string" | "number" | "bigint" | "boolean" | "symbol" | "undefined" | "object" | "function" // typeof null === "object" — JS quirk, handle separately function processValue(val: string | null) { if (typeof val === "object") { val; // null (since string wouldn't match "object") } }
instanceof Narrowing
function handleError(err: Error | string) { if (err instanceof TypeError) { err.message; // TypeError } else if (err instanceof Error) { err.stack; // Error } else { err; // string } } // Works with any class class Dog { bark() {} } class Cat { meow() {} } type Pet = Dog | Cat; function makeSound(pet: Pet) { if (pet instanceof Dog) pet.bark(); else pet.meow(); }
in Operator Narrowing
// Narrows based on property presence type Fish = { swim(): void }; type Bird = { fly(): void }; type FlyingFish = Fish & Bird; function move(animal: Fish | Bird) { if ("swim" in animal) animal.swim(); else animal.fly(); } // Also narrows optional properties type Admin = { role: "admin"; adminCode: string }; type User = { role: "user" }; function check(person: Admin | User) { if ("adminCode" in person) { person; // Admin } }
Discriminated Unions (Tagged Unions)
// Each variant has a shared literal discriminant property type Shape = | { kind: "circle"; radius: number } | { kind: "square"; side: number } | { kind: "rectangle"; width: number; height: number } | { kind: "triangle"; base: number; height: number }; function area(s: Shape): number { switch (s.kind) { case "circle": return Math.PI * s.radius ** 2; case "square": return s.side ** 2; case "rectangle": return s.width * s.height; case "triangle": return 0.5 * s.base * s.height; } } // Result type (common pattern) type Result<T, E = string> = | { success: true; value: T } | { success: false; error: E }; function divide(a: number, b: number): Result<number> { if (b === 0) return { success: false, error: "Division by zero" }; return { success: true, value: a / b }; } const r = divide(10, 2); if (r.success) r.value; // number else r.error; // string
Type Predicates (User-Defined Type Guards)
// Return type "val is T" narrows the type in the calling scope function isString(val: unknown): val is string { return typeof val === "string"; } function isError(val: unknown): val is Error { return val instanceof Error; } function isNonNull<T>(val: T | null | undefined): val is T { return val != null; } // Usage const values: unknown[] = [1, "two", null, "three"]; const strings = values.filter(isString); // string[] // Complex type predicate interface Cat { meow(): void } interface Dog { bark(): void } function isCat(pet: Cat | Dog): pet is Cat { return "meow" in pet; }
Assertion Functions
// asserts condition — narrows after the call function assert(condition: boolean, msg = "Assertion failed"): asserts condition { if (!condition) throw new Error(msg); } // asserts val is T — narrows the type of val after the call function assertDefined<T>(val: T | undefined | null): asserts val is T { if (val == null) throw new Error("Expected non-null"); } function assertIsString(val: unknown): asserts val is string { if (typeof val !== "string") throw new TypeError("Expected string"); } // Usage function processData(data: string | undefined) { assertDefined(data); data.toUpperCase(); // OK — data is string here } const input: unknown = getInput(); assertIsString(input); input.trim(); // OK — input is string
Equality Narrowing
// === narrows both sides to their intersection function example(x: string | number, y: string | boolean) { if (x === y) { x; // string (the only type both x and y share) y; // string } } // null/undefined checks function print(val: string | null | undefined) { if (val === null) { /* null */ } else if (val === undefined) { /* undefined */ } else val.toUpperCase(); // string // Shorthand for both if (val != null) val.toUpperCase(); // string (not null or undefined) } // switch on discriminant type Status = "active" | "inactive" | "pending"; function handle(s: Status) { switch (s) { case "active": return "running"; case "inactive": return "stopped"; case "pending": return "waiting"; } }
Control Flow Analysis
// TypeScript tracks types through branches function process(val: string | number | null): string { if (val === null) return "null"; // val: null here if (typeof val === "number") return val.toFixed(); // val: number here return val.toUpperCase(); // val: string here (null and number eliminated) } // Narrowing through assignments let x: string | number = Math.random() > 0.5 ? "hello" : 42; // x: string | number if (typeof x === "string") { x; // string x = 42; // assignment changes the narrowed type x; // number } // Narrowing in loops function processItems(items: (string | number)[]) { for (const item of items) { if (typeof item === "string") { item.toUpperCase(); // string } } }
Truthiness Narrowing
// Falsy values: false, 0, "", 0n, null, undefined, NaN function printName(name: string | null | undefined) { if (name) { name.toUpperCase(); // string (empty string also eliminated here!) } } // Be careful: truthiness excludes empty string and 0 function strictlyDefined(val: string | null | undefined) { if (val != null) { val; // string (null and undefined excluded, but "" allowed) } } // Boolean coercion in filter const arr: (string | null)[] = ["a", null, "b", null]; const strings = arr.filter(Boolean); // (string | null)[] — not narrowed const strings2 = arr.filter((x): x is string => x !== null); // string[]
Intersection Types and Narrowing
type Named = { name: string }; type Aged = { age: number }; type NamedAndAged = Named & Aged; // must have BOTH const p: NamedAndAged = { name: "Alice", age: 30 }; // Intersecting with discriminated unions type AdminUser = { role: "admin" } & Named; type RegularUser = { role: "user" } & Named; type AnyUser = AdminUser | RegularUser; function greet(user: AnyUser) { if (user.role === "admin") { user; // AdminUser } }
Exhaustive Checks with never
// After handling all cases, the type should be never type Animal = "cat" | "dog" | "bird"; function describe(a: Animal): string { switch (a) { case "cat": return "meow"; case "dog": return "woof"; case "bird": return "tweet"; default: const _: never = a; // compile error if a case is missing throw new Error(`Unhandled: ${_}`); } } // Utility function for exhaustive checks function assertNever(val: never, msg?: string): never { throw new Error(msg ?? `Unhandled value: ${JSON.stringify(val)}`); } function handleShape(s: Shape): number { switch (s.kind) { case "circle": return s.radius; case "square": return s.side; default: return assertNever(s); // TypeScript validates exhaustiveness } }
Narrowing with as const and Literal Types
// Widened type — loses literal info const dir = "left"; // type: string... no wait, const: "left" (literal) let dir2 = "left"; // type: string (widened because let) // Prevent widening in function calls function setDirection(dir: "left" | "right") {} const d = "left"; setDirection(d); // OK — const infers literal let d2 = "left"; // setDirection(d2); // Error — d2 is string, not "left" | "right" setDirection(d2 as "left" | "right"); // assertion workaround // as const in narrowing const config = { mode: "strict" } as const; config.mode; // "strict" (literal, not string)
Optional Chaining and Narrowing
interface User { name: string; address?: { city?: string; }; } function getCity(user: User): string { // Without optional chaining — manual narrowing if (user.address && user.address.city) { return user.address.city; // string } // With optional chaining — returns undefined if any link is null/undefined return user.address?.city ?? "Unknown"; } // Nullish coalescing in narrowing const value: string | null | undefined = getValue(); const result = value ?? "default"; // string (null/undefined cases handled)
Union Reduction and Extract/Exclude
type ABC = "a" | "b" | "c"; // Extract — keep only types assignable to U type AB = Extract<ABC, "a" | "b">; // "a" | "b" type Strings = Extract<string | number | boolean, string>; // string // Exclude — remove types assignable to U type C = Exclude<ABC, "a" | "b">; // "c" type NoStrings = Exclude<string | number | boolean, string>; // number | boolean // NonNullable — remove null and undefined type Defined = NonNullable<string | null | undefined>; // string // Practical: extract event subtypes type MouseEvents = Extract<Event, MouseEvent | PointerEvent>;