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>;