TypeScript Advanced Types

Learn conditional types, mapped types, template literals, and advanced pattern matching.

TL;DR

  1. 01Use conditional types for type-level if-else logic.
  2. 02Use mapped types to transform keys and values in existing types.
  3. 03Use template literal types for string manipulation at the type level.

Tips

  1. 01Use conditional types with infer to extract and reuse parts of complex types in your type transformations.

Warnings

  1. 01Advanced types are powerful but can become hard to read — document complex type transformations and keep them focused on single concerns.

Conditional Types

  • Write type-level if-else logic using the T extends X ? Y : Z ternary syntax.

    type IsString<T> = T extends string ? true : false;
    
    type A = IsString<"hello">;  // true
    type B = IsString<number>;   // false
    
  • Use infer to capture and reuse a type nested inside a generic structure.

    type Flatten<T> = T extends Array<infer U> ? U : T;
    
    type Str = Flatten<string[]>;  // string
    type Num = Flatten<number>;    // number
    
  • Extract function return types without importing the function itself.

    type ReturnType<T> = T extends (...args: any[]) => infer R ? R : never;
    
    type NumReturn = ReturnType<() => number>;  // number
    type StrReturn = ReturnType<() => string>;  // string
    
  • Distribute conditional types over union members automatically.

    type ToArray<T> = T extends any ? T[] : never;
    
    type Result = ToArray<string | number>;  // string[] | number[]
    
  • Prevent distribution by wrapping T in a tuple when you need one result.

    type IsUnion<T> = [T] extends [infer U] ? (U extends T ? false : true) : false;
    
    type A = IsUnion<string | number>;  // true
    type B = IsUnion<string>;           // false
    

Mapped Types

  • Transform every key in an object type by iterating over keyof.

    type MyReadonly<T> = {
      readonly [K in keyof T]: T[K];
    };
    
    type User = { name: string; age: number };
    type ReadonlyUser = MyReadonly<User>;
    // { readonly name: string; readonly age: number }
    
  • Make all properties optional by adding ? in a mapped type.

    type Optional<T> = {
      [K in keyof T]?: T[K];
    };
    
    type PartialUser = Optional<User>; // { name?: string; age?: number }
    
  • Rename keys with the as clause using template literal types.

    type Getters<T> = {
      [K in keyof T as `get${Capitalize<K & string>}`]: () => T[K];
    };
    
    type UserGetters = Getters<User>;
    // { getName: () => string; getAge: () => number }
    
  • Filter keys by combining mapped types with conditional types.

    type StringKeys<T> = {
      [K in keyof T as T[K] extends string ? K : never]: T[K];
    };
    
    type User = { id: number; name: string; email: string };
    type StringOnly = StringKeys<User>; // { name: string; email: string }
    
  • Build a Setters utility by inverting Getters with the mapped as pattern.

    type Setters<T> = {
      [K in keyof T as `set${Capitalize<K & string>}`]: (value: T[K]) => void;
    };
    
    type UserSetters = Setters<User>;
    // { setName: (value: string) => void; setAge: (value: number) => void }
    

Template Literal Types

  • Build string types at the type level using backtick template syntax.

    type Event<T extends string> = `on${Capitalize<T>}`;
    
    type ClickEvent = Event<"click">;   // "onClick"
    type ChangeEvent = Event<"change">; // "onChange"
    
  • Combine a union with a template to generate all string combinations.

    type Path = "user" | "post" | "comment";
    type GetRoute = `GET /${Path}`;
    // "GET /user" | "GET /post" | "GET /comment"
    
  • Extract the prefix of a string type with infer inside a template.

    type GetPrefix<T extends string> =
      T extends `${infer Prefix}_${string}` ? Prefix : never;
    
    type P = GetPrefix<"user_id">;    // "user"
    type Q = GetPrefix<"post_slug">;  // "post"
    
  • Split a string type into a tuple using recursive conditional types.

    type Split<T extends string, D extends string> =
      T extends `${infer F}${D}${infer R}` ? [F, ...Split<R, D>] : [T];
    
    type Parts = Split<"a-b-c", "-">;  // ["a", "b", "c"]
    
  • Build CSS-property-like types from a set of base names.

    type Side = "top" | "right" | "bottom" | "left";
    type Margin = `margin-${Side}`;
    // "margin-top" | "margin-right" | "margin-bottom" | "margin-left"
    

Advanced Utility Patterns

  • Build DeepPartial to make every nested property optional recursively.

    type DeepPartial<T> = T extends object
      ? { [K in keyof T]?: DeepPartial<T[K]> }
      : T;
    
  • Build DeepReadonly to freeze every level of a nested object type.

    type DeepReadonly<T> = T extends object
      ? { readonly [K in keyof T]: DeepReadonly<T[K]> }
      : T;
    
  • Check type assignability at the type level with a boolean predicate.

    type IsAssignableTo<T, U> = T extends U ? true : false;
    
    type A = IsAssignableTo<"hello", string>; // true
    type B = IsAssignableTo<number, string>;  // false
    
  • Build RequireKeys to make selected optional fields required.

    type RequireKeys<T, K extends keyof T> =
      Omit<T, K> & Required<Pick<T, K>>;
    
    type User = { id?: number; name?: string; email?: string };
    type UserWithId = RequireKeys<User, "id">;
    // { id: number; name?: string; email?: string }
    
  • Build a Validator type that maps each key to a validation function.

    type Validator<T> = {
      [K in keyof T]: (value: T[K]) => boolean;
    };
    
    // Usage: const userValidator: Validator<User> = { name: (v) => v.length > 0 };
    

Practical Applications

  • Build type-safe API response types using conditional types on status.

    type ApiResponse<T extends "success" | "error"> =
      T extends "success"
        ? { status: 200; data: unknown }
        : { status: 400; error: string };
    
    type Ok = ApiResponse<"success">; // { status: 200; data: unknown }
    
  • Create a database query builder type using mapped types with as.

    type SelectBuilder<T> = {
      [K in keyof T as `select${Capitalize<K & string>}`]: () => T[K];
    };
    
    // SelectBuilder<User> gives: { selectName: () => string; selectId: () => number }
    
  • Extract promise resolution types for async function return values.

    type Awaited<T> = T extends Promise<infer U> ? Awaited<U> : T;
    
    type Data = Awaited<Promise<Promise<string>>>; // string
    
  • Build a type-safe event map using template literals and mapped types.

    type Events = { click: MouseEvent; keydown: KeyboardEvent };
    type OnEvents = {
      [K in keyof Events as `on${Capitalize<K & string>}`]: (e: Events[K]) => void;
    };
    // { onClick: (e: MouseEvent) => void; onKeydown: (e: KeyboardEvent) => void }
    
  • Use conditional types to unwrap nested result types safely.

    type UnwrapResult<T> = T extends { ok: true; value: infer V }
      ? V
      : T extends { ok: false; error: infer E }
      ? never
      : never;
    

FAQ