Last Minute Notes

Advanced TypeScript Patterns

Production-grade TypeScript patterns for interviews.

📘 Advanced TypeScript Patterns (Production + Interview Focused)

Table of Contents

  1. Type Narrowing Patterns
  2. Exhaustive Checking with never
  3. Discriminated Unions
  4. Generic Utility Patterns
  5. Conditional Types
  6. Mapped Types in Production
  7. Keyof + Lookup Types
  8. Function Overloads in Real Code
  9. Type-safe Event Systems
  10. React-Specific Advanced Patterns
  11. API Response Safety Pattern
  12. Assertion Functions
  13. Preventing Invalid States
  14. Branded Types
  15. When NOT to Use Advanced Types
  16. What Interviewers Actually Look For
  17. One-Line Senior Summary

1. Type Narrowing Patterns (Critical for Interviews)

1.1 typeof Narrowing

function format(value: string | number) {
  if (typeof value === "string") {
    return value.toUpperCase();
  }
  return value.toFixed(2);
}

Why it matters:

  • Eliminates runtime errors
  • Compiler understands control flow

1.2 in Operator Narrowing

type Admin = { role: "admin"; permissions: string[] };
type User = { role: "user"; name: string };

function handleAccount(account: Admin | User) {
  if ("permissions" in account) {
    account.permissions.push("write");
  }
}

Used heavily with:

  • API responses
  • Role-based logic

1.3 instanceof Narrowing

class ApiError extends Error {
  statusCode: number = 500;
}

function handleError(error: unknown) {
  if (error instanceof ApiError) {
    console.log(error.statusCode);
  }
}

Flowchart: Which Narrowing to Use

Pick the check by what you can test at runtime, and the compiler narrows the type inside the branch.


2. Exhaustive Checking with never (Very Important)

Pattern

type Status = "loading" | "success" | "error";

function assertNever(x: never): never {
  throw new Error("Unhandled case");
}

function render(status: Status) {
  switch (status) {
    case "loading":
      return "Loading...";
    case "success":
      return "Success!";
    case "error":
      return "Error!";
    default:
      return assertNever(status);
  }
}

Why interviewers love this:

  • Guarantees future safety
  • Compiler error when new union member is added

Flowchart: How the never Check Works

After every case is handled, nothing is left for default, so its type is never.


3. Discriminated Unions (Most Asked Advanced Topic)

Core Pattern

type ApiResponse =
  | { status: "idle" }
  | { status: "loading" }
  | { status: "success"; data: string[] }
  | { status: "error"; error: string };

Usage:

function render(response: ApiResponse) {
  switch (response.status) {
    case "success":
      return response.data.map((item) => item);
    case "error":
      return response.error;
  }
}

Production usage:

  • API state management
  • Redux reducers
  • UI state machines

State diagram: A Union as a State Machine

Each variant of the union is one state, and only one is active at a time.


4. Generic Utility Patterns

4.1 Identity + Constraint Pattern

function byId<T extends { id: string }>(items: T[]): Record<string, T> {
  return Object.fromEntries(items.map((i) => [i.id, i]));
}

Why it matters:

  • Type-safe helpers
  • Reusable logic
  • No runtime checks needed

4.2 Generic Default Types

type ApiResult<T = unknown> = {
  data: T;
  error?: string;
};

Used in:

  • SDKs
  • API wrappers
  • Hooks

5. Conditional Types (Advanced but Real)

Basic Conditional Type

type IsString<T> = T extends string ? true : false;

Example:

type A = IsString<string>; // true
type B = IsString<number>; // false

Conditional Types with Inference (infer)

type ReturnTypeOf<T> = T extends (...args: any[]) => infer R ? R : never;

This is how built-in ReturnType works.

Flowchart: Conditional Types

A conditional type is an if over types, and infer captures a piece of the matched type.


6. Mapped Types in Production

6.1 Readonly Mapping

type Readonly<T> = {
  readonly [K in keyof T]: T[K];
};

6.2 Optional Mapping

type Optional<T> = {
  [K in keyof T]?: T[K];
};

6.3 API DTO Pattern

type ApiUser = {
  id: string;
  name: string;
  createdAt: string;
};

type UiUser = {
  id: string;
  name: string;
  createdAt: Date;
};

Conversion helper:

function mapUser(apiUser: ApiUser): UiUser {
  return {
    ...apiUser,
    createdAt: new Date(apiUser.createdAt),
  };
}

7. Keyof + Lookup Types (Interview Favorite)

type User = {
  id: number;
  name: string;
  isActive: boolean;
};

type UserValue<T extends keyof User> = User[T];

type NameType = UserValue<"name">; // string

Why this matters:

  • Type-safe property access
  • Used in forms, tables, configs

Flowchart: Lookup Type

The constraint T extends keyof User makes an invalid key a compile error.


8. Function Overloads in Real Code

API Helper Example

function fetchData(url: string): Promise<string>;
function fetchData(url: string, parseJson: true): Promise<object>;
function fetchData(url: string, parseJson?: boolean) {
  return fetch(url).then((res) => (parseJson ? res.json() : res.text()));
}

Used in:

  • SDKs
  • Utility libraries
  • Shared services

9. Type-safe Event Systems

type Events = {
  login: { userId: string };
  logout: undefined;
};

function emit<K extends keyof Events>(event: K, payload: Events[K]) {}

Usage:

emit("login", { userId: "123" });
emit("logout", undefined);

Very common in:

  • Analytics
  • Pub/sub
  • Feature flags

10. React-Specific Advanced Patterns

10.1 Generic Components

type ListProps<T> = {
  items: T[];
  render: (item: T) => React.ReactNode;
};

function List<T>({ items, render }: ListProps<T>) {
  return <>{items.map(render)}</>;
}

10.2 Polymorphic Components

type ButtonProps<T extends React.ElementType> = {
  as?: T;
} & React.ComponentPropsWithoutRef<T>;

function Button<T extends React.ElementType = "button">(props: ButtonProps<T>) {
  const { as: Component = "button", ...rest } = props;
  return <Component {...rest} />;
}

Used in:

  • Design systems
  • Component libraries

11. API Response Safety Pattern

type ApiSuccess<T> = {
  success: true;
  data: T;
};

type ApiError = {
  success: false;
  error: string;
};

type ApiResponse<T> = ApiSuccess<T> | ApiError;

Usage:

function handle(res: ApiResponse<User>) {
  if (res.success) {
    res.data.name;
  } else {
    res.error;
  }
}

12. Assertion Functions (asserts keyword)

function assertIsDefined<T>(value: T): asserts value is NonNullable<T> {
  if (value == null) {
    throw new Error("Value is null");
  }
}

Usage:

assertIsDefined(user);
user.name; // fully typed

Used in:

  • Runtime validation
  • Critical paths

Flowchart: Assertion Function

The compiler assumes the assertion held for every line after the call.


13. Preventing Invalid States (Senior-Level)

Bad

type State = {
  loading: boolean;
  data?: string[];
  error?: string;
};

Good

type State =
  | { status: "loading" }
  | { status: "success"; data: string[] }
  | { status: "error"; error: string };

This is a huge senior engineering signal.


14. Branded Types (Advanced Safety)

type UserId = string & { readonly brand: unique symbol };

function createUserId(id: string): UserId {
  return id as UserId;
}

Prevents:

const postId: string = "123";
const userId: UserId = postId; // ❌

Used in:

  • Finance
  • IDs
  • Security-sensitive apps

Flowchart: Branded Types

The brand exists only in the type system, so the only way to get a UserId is through the function that creates it.


15. When NOT to Use Advanced Types

Senior rule:

  • Avoid clever types if they hurt readability
  • Prefer simple interfaces
  • Types are for humans first, compiler second

16. What Interviewers Actually Look For

They check if you:

  • Model states correctly
  • Avoid impossible states
  • Use unknown safely
  • Use discriminated unions
  • Avoid any
  • Understand generics deeply

17. One-Line Senior Summary

Advanced TypeScript is about designing correct systems, not writing complex types.

On this page