Advanced120 min read

TypeScript Generics and Utility Types Deep Dive

Master TypeScript generics, conditional types, mapped types, and advanced utility type patterns for complex React applications.

Topics Covered:

GenericsConditional TypesMapped TypesTemplate Literal TypesType Utilities

Prerequisites:

  • Advanced TypeScript Patterns in React
  • Performance Optimization Techniques

Overview

This tutorial dives deep into TypeScript's advanced type system features. You'll learn how to create sophisticated generic types, use conditional types for type-level logic, leverage mapped types for transformations, and build reusable type utilities. These advanced features enable you to create type-safe abstractions, build better APIs, and catch errors at compile time that would otherwise only appear at runtime.

Lesson 1: Advanced Generics

Go beyond basic generics to create sophisticated type-safe abstractions. Advanced Generic Concepts: • Generic constraints (extends keyword) • Multiple type parameters • Default type parameters • Generic utility functions • Generic type inference Use Cases: • Type-safe API clients • Generic data structures • Flexible component APIs • Utility functions that work with any type

Code Example:
// Generic constraints
interface HasId {
  id: string | number;
}

function getById<T extends HasId>(items: T[], id: T["id"]): T | undefined {
  return items.find((item) => item.id === id);
}

// Multiple type parameters
function mapArray<T, U>(
  array: T[],
  mapper: (item: T) => U
): U[] {
  return array.map(mapper);
}

// Default type parameters
interface CacheOptions<T = string> {
  key: string;
  value: T;
  ttl?: number;
}

// Generic utility functions
function createApiClient<T extends Record<string, any>>(
  endpoints: T
) {
  return {
    async get<K extends keyof T>(
      endpoint: K,
      params?: Parameters<T[K]>[0]
    ): Promise<ReturnType<T[K]>> {
      // Implementation
      return {} as ReturnType<T[K]>;
    },
  };
}

// Usage
const api = createApiClient({
  users: () => ({ id: 1, name: "Alice" }),
  products: (id: number) => ({ id, title: "Widget" }),
});

const user = await api.get("users"); // Type-safe!
const product = await api.get("products", 123); // Type-safe!

Advanced generics use constraints to limit type parameters, multiple parameters for complex relationships, and default types for convenience. Generic utilities create powerful, type-safe abstractions.

Lesson 2: Conditional Types

Conditional types enable type-level logic - types that depend on other types. Conditional Type Syntax: • `T extends U ? X : Y` • Checks if T extends U • Returns X if true, Y if false Common Patterns: • Type extraction • Type filtering • Type transformation based on conditions • Utility type building Use Cases: • Extract return types • Filter union types • Create type-safe utilities • Build complex type systems

Code Example:
// Basic conditional type
type IsString<T> = T extends string ? true : false;

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

// Extract return type
type ReturnType<T> = T extends (...args: any[]) => infer R ? R : never;

// Extract function parameters
type Parameters<T> = T extends (...args: infer P) => any ? P : never;

// Non-nullable type
type NonNullable<T> = T extends null | undefined ? never : T;

// Extract array element type
type ArrayElement<T> = T extends (infer U)[] ? U : never;

type Numbers = ArrayElement<number[]>; // number

// Extract promise type
type Awaited<T> = T extends Promise<infer U> ? U : T;

type Result = Awaited<Promise<string>>; // string

// Filter types
type StringKeys<T> = {
  [K in keyof T]: T[K] extends string ? K : never;
}[keyof T];

interface User {
  id: number;
  name: string;
  email: string;
  age: number;
}

type UserStringKeys = StringKeys<User>; // "name" | "email"

// Exclude/Extract helpers
type Exclude<T, U> = T extends U ? never : T;
type Extract<T, U> = T extends U ? T : never;

type T1 = Exclude<"a" | "b" | "c", "a">; // "b" | "c"
type T2 = Extract<"a" | "b" | "c", "a" | "b">; // "a" | "b"

Conditional types perform type-level logic using the ternary operator. Use 'infer' to extract types from other types. Conditional types are powerful for building utility types and complex type transformations.

Lesson 3: Mapped Types

Mapped types create new types by transforming properties of existing types. Mapped Type Syntax: • `{ [K in keyof T]: ... }` • Iterates over keys of T • Transforms each property Common Patterns: • Make all properties optional/required • Make all properties readonly • Transform property types • Add/remove modifiers Built-in Mapped Types: • Partial<T> • Required<T> • Readonly<T> • Record<K, V> • Pick<T, K> • Omit<T, K>

Code Example:
// How built-in mapped types work

// Partial implementation
type MyPartial<T> = {
  [P in keyof T]?: T[P];
};

// Required implementation
type MyRequired<T> = {
  [P in keyof T]-?: T[P];
};

// Readonly implementation
type MyReadonly<T> = {
  readonly [P in keyof T]: T[P];
};

// Record implementation
type MyRecord<K extends keyof any, T> = {
  [P in K]: T;
};

// Custom mapped types
// Make all properties nullable
type Nullable<T> = {
  [P in keyof T]: T[P] | null;
};

// Transform to getters
type Getters<T> = {
  [P in keyof T as `get${Capitalize<string & P>}`]: () => T[P];
};

interface User {
  name: string;
  age: number;
}

type UserGetters = Getters<User>;
// {
//   getName: () => string;
//   getAge: () => number;
// }

// Filter properties by type
type StringProperties<T> = {
  [P in keyof T as T[P] extends string ? P : never]: T[P];
};

interface Person {
  id: number;
  name: string;
  email: string;
  age: number;
}

type PersonStrings = StringProperties<Person>;
// { name: string; email: string; }

// Add prefix to keys
type Prefixed<T, Prefix extends string> = {
  [P in keyof T as `${Prefix}${string & P}`]: T[P];
};

type PrefixedUser = Prefixed<User, "user_">;
// { user_name: string; user_age: number; }

Mapped types transform existing types by iterating over their properties. Use 'as' clauses to filter or rename keys. Mapped types enable powerful type transformations while maintaining type safety.

Lesson 4: Template Literal Types

Template literal types combine string literal types to create new string types. Features: • String concatenation at type level • Pattern matching • Type-safe string manipulation • Union type combinations Use Cases: • Type-safe CSS class names • API endpoint types • Event name types • Route types • ID generation

Code Example:
// Basic template literal types
type Greeting = `Hello, ${string}`;
type Email = `${string}@${string}.${string}`;

// With unions
type HttpMethod = "GET" | "POST" | "PUT" | "DELETE";
type Endpoint = "/users" | "/posts" | "/comments";

type ApiRoute = `${HttpMethod} ${Endpoint}`;
// "GET /users" | "POST /users" | "GET /posts" | ...

// Pattern matching
type ExtractMethod<T> = T extends `${infer M} ${string}` ? M : never;
type ExtractPath<T> = T extends `${string} ${infer P}` ? P : never;

type Method = ExtractMethod<"GET /users">; // "GET"
type Path = ExtractPath<"GET /users">; // "/users"

// CSS class utilities
type Spacing = "sm" | "md" | "lg";
type Direction = "top" | "bottom" | "left" | "right";

type MarginClass = `m${Capitalize<Direction>}-${Spacing}`;
// "mTop-sm" | "mTop-md" | "mBottom-sm" | ...

// Event types
type EventType = "click" | "change" | "submit";
type ElementType = "button" | "input" | "form";

type EventName = `${ElementType}${Capitalize<EventType>}`;
// "buttonClick" | "inputChange" | "formSubmit" | ...

// Utility type for event handlers
type EventHandlers<T extends EventName> = {
  [K in T]: (event: Event) => void;
};

const handlers: EventHandlers<EventName> = {
  buttonClick: (e) => {},
  inputChange: (e) => {},
  formSubmit: (e) => {},
};

Template literal types create new string types by combining literal types. Use 'infer' to extract parts of template literals. They're powerful for type-safe strings, CSS classes, API routes, and event names.

Lesson 5: Building Type Utilities

Combine all advanced TypeScript features to build reusable type utilities. Utility Building Principles: • Start with clear purpose • Use composition • Document with comments • Test with examples • Make it reusable Common Utilities: • Deep readonly • Deep partial • Optional by key • Required by key • Function parameter extraction

Code Example:
// Deep readonly
type DeepReadonly<T> = {
  readonly [P in keyof T]: T[P] extends object
    ? DeepReadonly<T[P]>
    : T[P];
};

// Deep partial
type DeepPartial<T> = {
  [P in keyof T]?: T[P] extends object ? DeepPartial<T[P]> : T[P];
};

// Make specific keys optional
type Optional<T, K extends keyof T> = Omit<T, K> & Partial<Pick<T, K>>;

interface User {
  id: number;
  name: string;
  email: string;
  role: string;
}

type UserWithoutRole = Optional<User, "role">;
// { id: number; name: string; email: string; role?: string; }

// Make specific keys required
type Required<T, K extends keyof T> = T & { [P in K]-?: T[P] };

// Extract function overloads
type Overloads<T> = T extends {
  (...args: infer A1): infer R1;
  (...args: infer A2): infer R2;
  (...args: infer A3): infer R3;
  (...args: infer A4): infer R4;
}
  ? [
      (...args: A1) => R1,
      (...args: A2) => R2,
      (...args: A3) => R3,
      (...args: A4) => R4
    ]
  : T extends {
      (...args: infer A1): infer R1;
      (...args: infer A2): infer R2;
      (...args: infer A3): infer R3;
    }
  ? [(...args: A1) => R1, (...args: A2) => R2, (...args: A3) => R3]
  : T extends {
      (...args: infer A1): infer R1;
      (...args: infer A2): infer R2;
    }
  ? [(...args: A1) => R1, (...args: A2) => R2]
  : T extends (...args: infer A) => infer R
  ? [(...args: A) => R]
  : never;

// React component prop utilities
type ComponentProps<T> = T extends React.ComponentType<infer P>
  ? P
  : never;

type ComponentPropsWithoutRef<T> = T extends React.ForwardRefExoticComponent<
  infer P
>
  ? P
  : React.ComponentProps<T>;

Build reusable type utilities by combining advanced TypeScript features. Start simple, compose utilities, and create powerful type transformations. These utilities can be shared across projects and improve type safety throughout your codebase.

Conclusion

Advanced TypeScript features enable sophisticated type-safe code. Generics provide flexibility, conditional types add type-level logic, mapped types transform types, and template literal types create precise string types. Combine these features to build powerful type utilities that catch errors at compile time. Remember: these advanced features are tools - use them when they add value, not just because they're possible. Start with simpler solutions and add complexity only when needed.