Back to blog

Deep Dive: Advanced TypeScript Types

typescriptadvanced-typesgenericstype-systemprogramming
Deep Dive: Advanced TypeScript Types

Welcome to the Deep Dive

You've learned TypeScript fundamentals — basic types, interfaces, generics, and utility types. Now it's time to go deeper. This post covers advanced type-level programming that separates everyday TypeScript from truly type-safe architectures.

These aren't academic exercises. Every pattern here solves a real problem: extracting types from functions, building type-safe event emitters, creating branded IDs that can't be mixed up, and generating types from string templates.

Prerequisite: Phase 1: TypeScript Fundamentals Time commitment: 2-3 hours (work through the examples)

What You'll Learn

✅ Conditional types with extends and infer
✅ Mapped types and key remapping
✅ Template literal types for string manipulation
✅ Recursive types for nested structures
✅ Discriminated unions and exhaustive checking
✅ Branded types for nominal typing
✅ Variadic tuple types
✅ Const assertions and satisfies operator
✅ Real-world pattern: type-safe API client


1. Conditional Types

Conditional types let you express type-level if/else logic. The syntax mirrors the ternary operator:

type IsString<T> = T extends string ? true : false;
 
type A = IsString<string>;  // true
type B = IsString<number>;  // false
type C = IsString<"hello">; // true (string literal extends string)

Distributive Conditional Types

When a conditional type acts on a union, it distributes over each member:

type ToArray<T> = T extends unknown ? T[] : never;
 
type Result = ToArray<string | number>;
// string[] | number[]  (NOT (string | number)[])

This is powerful for filtering unions:

type ExtractStrings<T> = T extends string ? T : never;
 
type Mixed = "hello" | 42 | "world" | true;
type OnlyStrings = ExtractStrings<Mixed>;
// "hello" | "world"

TypeScript's built-in Extract and Exclude utility types work exactly this way:

// Built-in definitions:
type Extract<T, U> = T extends U ? T : never;
type Exclude<T, U> = T extends U ? never : T;
 
type Numbers = Exclude<string | number | boolean, string>;
// number | boolean

Preventing Distribution

Sometimes you don't want distribution. Wrap both sides in tuples:

type IsNever<T> = [T] extends [never] ? true : false;
 
type A = IsNever<never>;        // true
type B = IsNever<string>;       // false
 
// Without the tuple wrapper, `never` distributes to `never`
type Broken<T> = T extends never ? true : false;
type C = Broken<never>;         // never (not true!)

2. The infer Keyword

infer lets you extract types from within conditional type checks. Think of it as pattern matching for types.

Extract Return Types

type ReturnOf<T> = T extends (...args: unknown[]) => infer R ? R : never;
 
type A = ReturnOf<() => string>;           // string
type B = ReturnOf<(x: number) => boolean>; // boolean
type C = ReturnOf<string>;                 // never

This is exactly how the built-in ReturnType<T> works.

Extract Parameter Types

type FirstParam<T> = T extends (first: infer P, ...rest: unknown[]) => unknown
  ? P
  : never;
 
type A = FirstParam<(name: string, age: number) => void>; // string
type B = FirstParam<() => void>;                           // never

Extract from Promise

type Awaited<T> = T extends Promise<infer U> ? Awaited<U> : T;
 
type A = Awaited<Promise<string>>;           // string
type B = Awaited<Promise<Promise<number>>>;  // number (recursive!)
type C = Awaited<string>;                    // string (not a promise)

Extract from Arrays

type ElementOf<T> = T extends (infer E)[] ? E : never;
 
type A = ElementOf<string[]>;          // string
type B = ElementOf<[string, number]>;  // string | number

Practical Example: Extract Event Payload

type EventMap = {
  "user:login": { userId: string; timestamp: Date };
  "user:logout": { userId: string };
  "order:created": { orderId: string; total: number };
};
 
type EventPayload<K extends keyof EventMap> = EventMap[K];
 
type LoginPayload = EventPayload<"user:login">;
// { userId: string; timestamp: Date }

3. Mapped Types

Mapped types transform every property of an existing type. You've already used built-in ones like Partial<T> and Readonly<T> — here's how they work under the hood.

Basic Mapped Types

// Make all properties optional
type MyPartial<T> = {
  [K in keyof T]?: T[K];
};
 
// Make all properties readonly
type MyReadonly<T> = {
  readonly [K in keyof T]: T[K];
};
 
// Make all properties required
type MyRequired<T> = {
  [K in keyof T]-?: T[K];
};

The -? modifier removes optionality. Similarly, -readonly removes readonly.

Transforming Property Values

interface User {
  name: string;
  age: number;
  email: string;
}
 
// Wrap every property in a getter function
type Getters<T> = {
  [K in keyof T as `get${Capitalize<string & K>}`]: () => T[K];
};
 
type UserGetters = Getters<User>;
// {
//   getName: () => string;
//   getAge: () => number;
//   getEmail: () => string;
// }

Key Remapping with as

You can rename keys during mapping:

// Prefix all keys with "on"
type Listeners<T> = {
  [K in keyof T as `on${Capitalize<string & K>}Change`]: (
    value: T[K]
  ) => void;
};
 
type UserListeners = Listeners<User>;
// {
//   onNameChange: (value: string) => void;
//   onAgeChange: (value: number) => void;
//   onEmailChange: (value: string) => void;
// }

Filtering Properties

Return never as the key to filter out properties:

// Keep only string properties
type StringProps<T> = {
  [K in keyof T as T[K] extends string ? K : never]: T[K];
};
 
type UserStrings = StringProps<User>;
// { name: string; email: string }

Practical Example: Form Validation

interface LoginForm {
  email: string;
  password: string;
  rememberMe: boolean;
}
 
type FormErrors<T> = {
  [K in keyof T]?: string[];
};
 
type FormTouched<T> = {
  [K in keyof T]?: boolean;
};
 
type FormState<T> = {
  values: T;
  errors: FormErrors<T>;
  touched: FormTouched<T>;
  isValid: boolean;
  isSubmitting: boolean;
};
 
const loginState: FormState<LoginForm> = {
  values: { email: "", password: "", rememberMe: false },
  errors: { email: ["Email is required"] },
  touched: { email: true },
  isValid: false,
  isSubmitting: false,
};

4. Template Literal Types

Template literal types let you build types from string patterns — the same template literal syntax you use in JavaScript, but at the type level.

Basic String Manipulation

type Greeting = `Hello, ${string}`;
 
const a: Greeting = "Hello, World";     // OK
const b: Greeting = "Hello, TypeScript"; // OK
// const c: Greeting = "Hi, World";     // Error!
 
// Combine with unions for combinations
type Color = "red" | "green" | "blue";
type Opacity = "light" | "dark";
type Theme = `${Opacity}-${Color}`;
// "light-red" | "light-green" | "light-blue" | "dark-red" | "dark-green" | "dark-blue"

Built-in String Manipulation Types

TypeScript provides four intrinsic string manipulation types:

type Upper = Uppercase<"hello">;     // "HELLO"
type Lower = Lowercase<"HELLO">;     // "hello"
type Cap = Capitalize<"hello">;      // "Hello"
type Uncap = Uncapitalize<"Hello">;  // "hello"

Pattern Matching with Template Literals

Combine with infer to parse strings at the type level:

// Extract path parameters from a route string
type ExtractParams<T extends string> =
  T extends `${string}:${infer Param}/${infer Rest}`
    ? Param | ExtractParams<Rest>
    : T extends `${string}:${infer Param}`
      ? Param
      : never;
 
type Params = ExtractParams<"/users/:userId/posts/:postId">;
// "userId" | "postId"

Practical Example: Type-Safe Event Emitter

type EventName = `${string}:${string}`;
 
type SplitEvent<T extends string> =
  T extends `${infer Domain}:${infer Action}`
    ? { domain: Domain; action: Action }
    : never;
 
type Parsed = SplitEvent<"user:login">;
// { domain: "user"; action: "login" }
 
// Type-safe CSS property builder
type CSSUnit = "px" | "rem" | "em" | "%" | "vh" | "vw";
type CSSValue = `${number}${CSSUnit}`;
 
function setWidth(value: CSSValue): void {
  console.log(`Setting width: ${value}`);
}
 
setWidth("100px");   // OK
setWidth("2.5rem");  // OK
// setWidth("100");  // Error: not a valid CSSValue

5. Recursive Types

Recursive types reference themselves — essential for modeling nested data structures like trees, JSON, and deeply nested objects.

JSON Type

type JSONValue =
  | string
  | number
  | boolean
  | null
  | JSONValue[]
  | { [key: string]: JSONValue };
 
// This type accurately describes any valid JSON value
const data: JSONValue = {
  name: "Alice",
  age: 30,
  hobbies: ["reading", "coding"],
  address: {
    city: "Portland",
    zip: "97201",
  },
};

Deep Readonly

type DeepReadonly<T> = T extends (infer E)[]
  ? ReadonlyArray<DeepReadonly<E>>
  : T extends object
    ? { readonly [K in keyof T]: DeepReadonly<T[K]> }
    : T;
 
interface Config {
  database: {
    host: string;
    port: number;
    credentials: {
      username: string;
      password: string;
    };
  };
  features: string[];
}
 
type ReadonlyConfig = DeepReadonly<Config>;
// All nested properties are readonly — including array elements

Deep Partial

type DeepPartial<T> = T extends object
  ? { [K in keyof T]?: DeepPartial<T[K]> }
  : T;
 
// Useful for configuration overrides
function mergeConfig(
  base: Config,
  overrides: DeepPartial<Config>
): Config {
  return {
    ...base,
    ...overrides,
    database: {
      ...base.database,
      ...overrides.database,
      credentials: {
        ...base.database.credentials,
        ...overrides.database?.credentials,
      },
    },
    features: overrides.features ?? base.features,
  };
}
 
// Only override what you need
mergeConfig(defaultConfig, {
  database: { port: 5433 },
});

Flatten Nested Arrays

type Flatten<T> = T extends (infer E)[] ? Flatten<E> : T;
 
type A = Flatten<number[][][]>;  // number
type B = Flatten<string[]>;      // string
type C = Flatten<boolean>;       // boolean

Dot-Notation Paths

A common need: get all valid dot-notation paths for a nested object:

type Paths<T, Prefix extends string = ""> = T extends object
  ? {
      [K in keyof T & string]: T[K] extends object
        ? `${Prefix}${K}` | Paths<T[K], `${Prefix}${K}.`>
        : `${Prefix}${K}`;
    }[keyof T & string]
  : never;
 
interface Settings {
  theme: {
    mode: "light" | "dark";
    colors: {
      primary: string;
      secondary: string;
    };
  };
  notifications: {
    email: boolean;
    push: boolean;
  };
}
 
type SettingPaths = Paths<Settings>;
// "theme" | "theme.mode" | "theme.colors" | "theme.colors.primary"
// | "theme.colors.secondary" | "notifications" | "notifications.email"
// | "notifications.push"

6. Discriminated Unions

Discriminated unions (also called tagged unions) are one of the most practical type patterns. A shared literal property (the "discriminant") lets TypeScript narrow the type automatically.

Basic Pattern

type Shape =
  | { kind: "circle"; radius: number }
  | { kind: "rectangle"; width: number; height: number }
  | { kind: "triangle"; base: number; height: number };
 
function area(shape: Shape): number {
  switch (shape.kind) {
    case "circle":
      return Math.PI * shape.radius ** 2;
    case "rectangle":
      return shape.width * shape.height;
    case "triangle":
      return (shape.base * shape.height) / 2;
  }
}

Exhaustive Checking

Ensure you handle every variant with the never trick:

function assertNever(value: never): never {
  throw new Error(`Unexpected value: ${JSON.stringify(value)}`);
}
 
function area(shape: Shape): number {
  switch (shape.kind) {
    case "circle":
      return Math.PI * shape.radius ** 2;
    case "rectangle":
      return shape.width * shape.height;
    case "triangle":
      return (shape.base * shape.height) / 2;
    default:
      return assertNever(shape);
      // If you add a new shape variant without handling it,
      // TypeScript will error here at compile time
  }
}

Result Type (Error Handling)

type Result<T, E = Error> =
  | { ok: true; value: T }
  | { ok: false; error: E };
 
function divide(a: number, b: number): Result<number, string> {
  if (b === 0) {
    return { ok: false, error: "Division by zero" };
  }
  return { ok: true, value: a / b };
}
 
const result = divide(10, 3);
if (result.ok) {
  console.log(result.value); // TypeScript knows this is number
} else {
  console.error(result.error); // TypeScript knows this is string
}

API Response Pattern

type ApiResponse<T> =
  | { status: "loading" }
  | { status: "success"; data: T }
  | { status: "error"; error: string; statusCode: number };
 
function renderUser(response: ApiResponse<User>) {
  switch (response.status) {
    case "loading":
      return <Spinner />;
    case "success":
      return <UserCard user={response.data} />;
    case "error":
      return <ErrorMessage message={response.error} />;
  }
}

7. Branded Types

TypeScript uses structural typing — if two types have the same shape, they're compatible. This can cause bugs when you want to distinguish between conceptually different values.

The Problem

type UserId = string;
type OrderId = string;
 
function getUser(id: UserId): User { /* ... */ }
function getOrder(id: OrderId): Order { /* ... */ }
 
const userId: UserId = "user_123";
const orderId: OrderId = "order_456";
 
// This compiles but is WRONG — you're passing an order ID to getUser!
getUser(orderId); // No error! Both are just `string`

The Solution: Branded Types

Add a phantom property that exists only at the type level:

// Brand utility
type Brand<T, B extends string> = T & { readonly __brand: B };
 
type UserId = Brand<string, "UserId">;
type OrderId = Brand<string, "OrderId">;
 
// Constructor functions
function createUserId(id: string): UserId {
  // Add validation here if needed
  return id as UserId;
}
 
function createOrderId(id: string): OrderId {
  return id as OrderId;
}
 
function getUser(id: UserId): void {
  console.log(`Getting user: ${id}`);
}
 
const userId = createUserId("user_123");
const orderId = createOrderId("order_456");
 
getUser(userId);   // OK
// getUser(orderId); // Error: Type 'OrderId' is not assignable to type 'UserId'

Branded Numeric Types

type Meters = Brand<number, "Meters">;
type Kilometers = Brand<number, "Kilometers">;
type Celsius = Brand<number, "Celsius">;
type Fahrenheit = Brand<number, "Fahrenheit">;
 
function metersToKilometers(m: Meters): Kilometers {
  return (m / 1000) as Kilometers;
}
 
function celsiusToFahrenheit(c: Celsius): Fahrenheit {
  return (c * 9 / 5 + 32) as Fahrenheit;
}
 
const distance = 5000 as Meters;
const km = metersToKilometers(distance); // OK
 
const temp = 100 as Celsius;
// metersToKilometers(temp); // Error! Can't pass Celsius as Meters

Validated Strings

type Email = Brand<string, "Email">;
type URL = Brand<string, "URL">;
 
function validateEmail(input: string): Email | null {
  const emailRegex = /^[^\s@]+@[^\s@]+\.[^\s@]+$/;
  return emailRegex.test(input) ? (input as Email) : null;
}
 
function sendEmail(to: Email, subject: string): void {
  // `to` is guaranteed to be a validated email
  console.log(`Sending "${subject}" to ${to}`);
}
 
const email = validateEmail("alice@example.com");
if (email) {
  sendEmail(email, "Welcome!"); // OK — validated
}
// sendEmail("not-validated", "Oops"); // Error!

8. Variadic Tuple Types

Variadic tuple types (TypeScript 4.0+) let you work with tuples of unknown length while preserving type information.

Spread in Tuple Types

type Concat<A extends unknown[], B extends unknown[]> = [...A, ...B];
 
type AB = Concat<[string, number], [boolean]>;
// [string, number, boolean]

Typed Function Composition

type Head<T extends unknown[]> = T extends [infer H, ...unknown[]] ? H : never;
type Tail<T extends unknown[]> = T extends [unknown, ...infer Rest] ? Rest : [];
type Last<T extends unknown[]> = T extends [...unknown[], infer L] ? L : never;
 
type A = Head<[string, number, boolean]>;  // string
type B = Tail<[string, number, boolean]>;  // [number, boolean]
type C = Last<[string, number, boolean]>;  // boolean

Practical Example: Typed Middleware Pipeline

type Middleware<In, Out> = (input: In) => Out;
 
function pipe<A, B>(m1: Middleware<A, B>): Middleware<A, B>;
function pipe<A, B, C>(
  m1: Middleware<A, B>,
  m2: Middleware<B, C>
): Middleware<A, C>;
function pipe<A, B, C, D>(
  m1: Middleware<A, B>,
  m2: Middleware<B, C>,
  m3: Middleware<C, D>
): Middleware<A, D>;
function pipe(...fns: Middleware<unknown, unknown>[]): Middleware<unknown, unknown> {
  return (input) => fns.reduce((acc, fn) => fn(acc), input);
}
 
const transform = pipe(
  (input: string) => input.length,        // string → number
  (length: number) => length > 5,         // number → boolean
  (isLong: boolean) => isLong ? "yes" : "no" // boolean → string
);
 
const result = transform("TypeScript"); // "yes"

9. Const Assertions and satisfies

as const — Const Assertions

as const makes TypeScript infer the narrowest possible type:

// Without as const
const colors = ["red", "green", "blue"];
// type: string[]
 
// With as const
const colors = ["red", "green", "blue"] as const;
// type: readonly ["red", "green", "blue"]
 
// Without as const
const config = { mode: "production", port: 3000 };
// type: { mode: string; port: number }
 
// With as const
const config = { mode: "production", port: 3000 } as const;
// type: { readonly mode: "production"; readonly port: 3000 }

Derive Types from Constants

const ROLES = ["admin", "editor", "viewer"] as const;
type Role = (typeof ROLES)[number]; // "admin" | "editor" | "viewer"
 
const STATUS_MAP = {
  active: "Active",
  inactive: "Inactive",
  pending: "Pending Review",
} as const;
 
type StatusKey = keyof typeof STATUS_MAP;     // "active" | "inactive" | "pending"
type StatusLabel = (typeof STATUS_MAP)[StatusKey]; // "Active" | "Inactive" | "Pending Review"

satisfies — Type Check Without Widening

The satisfies operator (TypeScript 4.9+) validates that a value matches a type without losing the specific type information:

type ColorMap = Record<string, string | number[]>;
 
// Using type annotation — loses specifics
const colorsAnnotated: ColorMap = {
  red: "#ff0000",
  green: [0, 255, 0],
};
colorsAnnotated.red.toUpperCase(); // Error! Could be string | number[]
 
// Using satisfies — keeps specifics
const colorsSatisfied = {
  red: "#ff0000",
  green: [0, 255, 0],
} satisfies ColorMap;
 
colorsSatisfied.red.toUpperCase();  // OK! TypeScript knows it's a string
colorsSatisfied.green.map(x => x);  // OK! TypeScript knows it's number[]

Combining as const and satisfies

type Route = {
  path: string;
  method: "GET" | "POST" | "PUT" | "DELETE";
};
 
const routes = {
  getUsers: { path: "/users", method: "GET" },
  createUser: { path: "/users", method: "POST" },
  getUser: { path: "/users/:id", method: "GET" },
} as const satisfies Record<string, Route>;
 
// Type-safe access with exact literal types
type GetUsersPath = (typeof routes)["getUsers"]["path"]; // "/users"
type GetUsersMethod = (typeof routes)["getUsers"]["method"]; // "GET"

10. Real-World Pattern: Type-Safe API Client

Let's combine everything into a practical example — a type-safe API client that infers request and response types from a route definition.

Step 1: Define the API Schema

interface ApiSchema {
  "GET /users": {
    response: User[];
    query: { page?: number; limit?: number };
  };
  "GET /users/:id": {
    response: User;
    params: { id: string };
  };
  "POST /users": {
    response: User;
    body: { name: string; email: string };
  };
  "PUT /users/:id": {
    response: User;
    params: { id: string };
    body: Partial<{ name: string; email: string }>;
  };
  "DELETE /users/:id": {
    response: void;
    params: { id: string };
  };
}
 
interface User {
  id: string;
  name: string;
  email: string;
  createdAt: string;
}

Step 2: Extract Types from Routes

// Extract HTTP method from route key
type ExtractMethod<T extends string> =
  T extends `${infer M} ${string}` ? M : never;
 
// Extract path from route key
type ExtractPath<T extends string> =
  T extends `${string} ${infer P}` ? P : never;
 
// Extract path parameters
type ExtractPathParams<T extends string> =
  T extends `${string}:${infer Param}/${infer Rest}`
    ? Param | ExtractPathParams<Rest>
    : T extends `${string}:${infer Param}`
      ? Param
      : never;
 
// Test it
type Method = ExtractMethod<"GET /users/:id">; // "GET"
type Path = ExtractPath<"GET /users/:id">;       // "/users/:id"
type Params = ExtractPathParams<"/users/:id">;   // "id"

Step 3: Build the Client

type ApiEndpoint = keyof ApiSchema;
 
type RequestOptions<E extends ApiEndpoint> = {
  params: "params" extends keyof ApiSchema[E]
    ? ApiSchema[E]["params"]
    : never;
  query: "query" extends keyof ApiSchema[E]
    ? ApiSchema[E]["query"]
    : never;
  body: "body" extends keyof ApiSchema[E]
    ? ApiSchema[E]["body"]
    : never;
};
 
// Remove `never` fields so callers don't need to pass them
type CleanOptions<T> = {
  [K in keyof T as T[K] extends never ? never : K]: T[K];
};
 
type FetchOptions<E extends ApiEndpoint> = CleanOptions<RequestOptions<E>>;
 
async function apiClient<E extends ApiEndpoint>(
  endpoint: E,
  ...args: keyof FetchOptions<E> extends never
    ? []
    : [options: FetchOptions<E>]
): Promise<ApiSchema[E]["response"]> {
  const [method, path] = (endpoint as string).split(" ");
  let url = path;
 
  const options = args[0] as Record<string, unknown> | undefined;
 
  // Replace path params
  if (options && "params" in options) {
    const params = options.params as Record<string, string>;
    for (const [key, value] of Object.entries(params)) {
      url = url.replace(`:${key}`, value);
    }
  }
 
  // Add query params
  if (options && "query" in options) {
    const query = options.query as Record<string, string>;
    const searchParams = new URLSearchParams(query);
    url += `?${searchParams.toString()}`;
  }
 
  const response = await fetch(`/api${url}`, {
    method,
    headers: { "Content-Type": "application/json" },
    body: options && "body" in options
      ? JSON.stringify(options.body)
      : undefined,
  });
 
  return response.json();
}

Step 4: Use the Client

// GET /users — optional query params
const users = await apiClient("GET /users", {
  query: { page: 1, limit: 10 },
});
// users: User[]
 
// GET /users/:id — requires params
const user = await apiClient("GET /users/:id", {
  params: { id: "123" },
});
// user: User
 
// POST /users — requires body
const newUser = await apiClient("POST /users", {
  body: { name: "Alice", email: "alice@example.com" },
});
// newUser: User
 
// DELETE /users/:id — requires params
await apiClient("DELETE /users/:id", {
  params: { id: "123" },
});
// void
 
// Type errors caught at compile time:
// apiClient("GET /users/:id"); // Error: missing params
// apiClient("POST /users", { body: { name: 123 } }); // Error: name must be string
// apiClient("PATCH /users"); // Error: endpoint doesn't exist

Every route is fully typed — params, query, body, and response. Add a new route to ApiSchema and the client automatically knows how to call it.


Common Pitfalls

1. Excessive Type Complexity

Not every problem needs advanced types. If a type takes more than 10 seconds to understand, consider simplifying:

// Too complex — hard to read and maintain
type DeepMerge<A, B> = {
  [K in keyof A | keyof B]: K extends keyof A & keyof B
    ? A[K] extends object
      ? B[K] extends object
        ? DeepMerge<A[K], B[K]>
        : B[K]
      : B[K]
    : K extends keyof B
      ? B[K]
      : K extends keyof A
        ? A[K]
        : never;
};
 
// Often simpler: just use a runtime function with a reasonable return type
function deepMerge<T extends object>(base: T, overrides: DeepPartial<T>): T {
  // ... implementation
}

2. Circular Type References

TypeScript has a recursion depth limit. Deep recursion can cause:

// This can cause "Type instantiation is excessively deep" error
type DeepPath<T, Depth extends number[] = []> =
  Depth["length"] extends 10 ? never : // Add a depth limit
  T extends object
    ? { [K in keyof T & string]:
        | K
        | `${K}.${DeepPath<T[K], [...Depth, 0]>}` }[keyof T & string]
    : never;

3. Overusing as

Type assertions (as) bypass the type checker. Every as is a potential bug:

// Avoid
const data = fetchData() as User;
 
// Prefer: validate at runtime
function isUser(data: unknown): data is User {
  return (
    typeof data === "object" &&
    data !== null &&
    "id" in data &&
    "name" in data &&
    "email" in data
  );
}
 
const data = fetchData();
if (isUser(data)) {
  // TypeScript knows data is User
  console.log(data.name);
}

Summary and Key Takeaways

Conditional types: Type-level if/else with extends — handles unions distributively
infer keyword: Extract types from complex structures — return types, parameters, promises
Mapped types: Transform all properties of a type — with key remapping and filtering
Template literal types: String manipulation at the type level — parse routes, build event names
Recursive types: Model nested data — JSON, deep readonly, dot-notation paths
Discriminated unions: Type-safe switch/case with exhaustive checking
Branded types: Nominal typing to prevent ID/unit mixups
Variadic tuples: Work with tuples of unknown length
as const + satisfies: Narrow constants while validating against a type

When to Use Advanced Types

PatternUse When
Conditional typesBuilding type utilities, filtering unions
inferExtracting types from functions, promises, arrays
Mapped typesTransforming API interfaces, form types
Template literalsRoute typing, event systems, CSS-in-JS
Branded typesIDs, units, validated strings
Discriminated unionsState machines, API responses, action types

What's Next?

Now that you've mastered advanced types, apply them to real frameworks:

Additional Resources


Series: TypeScript Full-Stack Roadmap Previous: Phase 4: Full-Stack Integration Next: Deep Dive: React with TypeScript

Happy coding! 🚀

📬 Subscribe to Newsletter

Get the latest blog posts delivered to your inbox every week. No spam, unsubscribe anytime.

We respect your privacy. Unsubscribe at any time.

💬 Comments

Sign in to leave a comment

We'll never post without your permission.