06 - Advanced Topics

Type Guards

Narrow a type at runtime so TypeScript knows what you're working with:

// typeof guard — for primitives
function format(value: string | number): string {
  if (typeof value === "string") {
    return value.toUpperCase();  // TypeScript knows it's a string here
  }
  return value.toFixed(2);       // TypeScript knows it's a number here
}

// Custom type guard — for complex types
function isString(value: unknown): value is string {
  return typeof value === "string";
}

function process(value: unknown) {
  if (isString(value)) {
    console.log(value.toUpperCase());  // ✅ TypeScript knows it's a string
  }
}

// in guard — check if a property exists
interface Dog { bark(): void; }
interface Cat { meow(): void; }

function speak(animal: Dog | Cat) {
  if ("bark" in animal) {
    animal.bark();   // TypeScript knows it's a Dog
  } else {
    animal.meow();   // TypeScript knows it's a Cat
  }
}

Utility Types

Built-in types that transform other types — you'll use these constantly:

Utility What it does
Partial<T> Makes all properties optional
Required<T> Makes all properties required
Readonly<T> Prevents properties from being reassigned
Pick<T, K> Keeps only the specified properties
Omit<T, K> Removes the specified properties
Record<K, V> Creates an object type with keys K and values V
ReturnType<T> Extracts a function's return type
Parameters<T> Extracts a function's parameter types as a tuple
interface User {
  id: number;
  name: string;
  email: string;
}

type PartialUser = Partial<User>;          // all properties optional
type RequiredUser = Required<User>;        // all properties required
type ReadonlyUser = Readonly<User>;        // all properties readonly
type UserWithoutEmail = Omit<User, "email">;  // remove specific properties
type UserIdAndName = Pick<User, "id" | "name">;  // keep only specific properties
type UserRecord = Record<string, User>;    // { [key: string]: User }

Common use case — update functions:

// Partial<User> means all fields are optional — update only what you pass
function updateUser(id: number, updates: Partial<User>): void {
  // ...
}

updateUser(1, { name: "Bob" });          // ✅ only update name
updateUser(1, { name: "Bob", email: "b@b.com" });  // ✅ update name and email

Enums

Named constants — useful for fixed sets of values:

// Numeric enum (auto-increments from 0)
enum Direction {
  Up,      // 0
  Down,    // 1
  Left,    // 2
  Right    // 3
}

const move = Direction.Up;  // 0

// String enum (explicit values — preferred for readability)
enum Status {
  Active = "ACTIVE",
  Inactive = "INACTIVE",
  Pending = "PENDING"
}

function setStatus(status: Status) { /* ... */ }
setStatus(Status.Active);     // ✅
setStatus("ACTIVE");          // ❌ error — must use the enum

Alternative: many prefer union types over enums (simpler, no runtime code):

type Status = "active" | "inactive" | "pending";

Type Assertions

Tell TypeScript "I know the type better than you":

// as syntax (preferred)
const input = document.getElementById("input") as HTMLInputElement;
input.value;  // ✅ TypeScript knows it's an input element

// angle bracket syntax (same thing, doesn't work in JSX/Svelte)
const input2 = <HTMLInputElement>document.getElementById("input");

Use sparingly — you're overriding TypeScript's type checking.

Non-null Assertion

The ! operator tells TypeScript a value is definitely not null/undefined:

function process(value: string | null) {
  console.log(value!.toUpperCase());  // "trust me, it's not null"
}

Prefer optional chaining (?.) or null checks instead — ! can hide bugs.

Discriminated Unions

A pattern for handling different shapes with a shared "tag" property:

interface Success {
  type: "success";  // the discriminant
  data: string;
}

interface Error {
  type: "error";    // the discriminant
  message: string;
}

type Result = Success | Error;

function handle(result: Result) {
  switch (result.type) {
    case "success":
      console.log(result.data);     // TypeScript knows it's Success
      break;
    case "error":
      console.log(result.message);  // TypeScript knows it's Error
      break;
  }
}

This pattern is used everywhere — API responses, state management, event handling.

Mapped Types

Transform properties of an existing type:

// Make all properties optional (this is how Partial<T> works internally)
type MyPartial<T> = {
  [K in keyof T]?: T[K];
};

// Make all properties readonly
type MyReadonly<T> = {
  readonly [K in keyof T]: T[K];
};

The infer Keyword

infer lets you extract a type from within a conditional type — like pattern matching for types:

// Extract the return type of a function
type GetReturn<T> = T extends (...args: any[]) => infer R ? R : never;

type A = GetReturn<() => string>;        // string
type B = GetReturn<(x: number) => boolean>;  // boolean

infer R says "whatever type appears here, capture it as R." This is how built-in utilities like ReturnType<T> and Parameters<T> work internally.

// Extract the element type from an array
type ElementOf<T> = T extends (infer E)[] ? E : never;

type C = ElementOf<string[]>;   // string
type D = ElementOf<number[]>;   // number

Template Literal Types

Create string pattern types using template literal syntax:

type Color = "red" | "blue";
type Size = "small" | "large";

type Combo = `${Size}-${Color}`;
// "small-red" | "small-blue" | "large-red" | "large-blue"

let valid: Combo = "small-red";   // ✅
let invalid: Combo = "medium-red"; // ❌ error

TypeScript generates all possible combinations from the unions — useful for CSS classes, event names, or any structured string patterns.

Key Takeaways

  • Type guards (typeof, in, custom) narrow types at runtime
  • Utility types (Partial, Omit, Pick, Record, ReturnType, Parameters) transform existing types
  • Discriminated unions use a shared "tag" property for type-safe branching
  • Prefer union types ("a" | "b") over enums for simple cases
  • Use as assertions sparingly — prefer letting TypeScript infer
  • ! (non-null assertion) is risky — prefer ?. (optional chaining)
  • Mapped types let you transform properties of existing types
  • infer extracts types from within conditional types
  • Template literal types create string patterns from unions