🔷 TypeScript Q53 / 101

Explain advanced generics patterns.

AI-Powered Answer ✓ Answered

Advanced generics in TypeScript empower developers to write highly flexible, type-safe, and reusable code. By understanding and applying patterns like conditional types with `infer`, mapped types, template literal types, variadic tuple types, and recursive generics, you can create powerful abstractions that precisely model complex data structures and behaviors at the type level.

1. Conditional Types with `infer`

Conditional types allow you to select one of two possible types based on a condition expressed as a type relationship test. The infer keyword takes this a step further, enabling you to extract parts of a type within the extends clause of a conditional type, effectively creating new type variables.

typescript
type ReturnType<T extends (...args: any) => any> = T extends (...args: any) => infer R ? R : never;

// Example Usage:
function greet(name: string, age: number): string { return `Hello ${name}, you are ${age}.`; }
type GreetReturnType = ReturnType<typeof greet>; // string

type PromiseValue<T> = T extends Promise<infer U> ? U : T;

type ResolvedString = PromiseValue<Promise<string>>; // string
type ResolvedNumber = PromiseValue<number>;         // number

In ReturnType, infer R captures the return type of the function T. In PromiseValue, infer U extracts the type that the promise resolves to. This is crucial for creating utility types that introspect and manipulate existing types.

2. Mapped Types

Mapped types allow you to create new object types based on existing ones by iterating over the properties of a type and transforming them. They are defined using a syntax similar to array mapping or object iteration in JavaScript, but at the type level.

typescript
type Nullable<T> = {
  [P in keyof T]: T[P] | null;
};

// Example Usage:
interface User {
  id: number;
  name: string;
  email?: string;
}

type NullableUser = Nullable<User>;
/*
{
  id: number | null;
  name: string | null;
  email?: string | null | undefined; // 'email' keeps its optionality unless explicitly removed
}
*/

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

type UserGetters = Getters<User>;
/*
{
  getId: () => number;
  getName: () => string;
  getEmail?: () => string | undefined; // Also keeps optionality
}
*/

Nullable<T> iterates over each property P in T and makes its type T[P] | null. The as clause in Getters<T> re-maps the property keys themselves, combining mapped types with template literal types to transform id into getId, name into getName, etc.

3. Template Literal Types

Template Literal Types allow you to create new string literal types by concatenating string literals with union types of string literals, number literals, or bigint literals. They work like JavaScript template literals but operate on types, enabling powerful string manipulation at the type level.

typescript
type EventName<T extends string> = `${T}Changed` | `${T}Deleted`;

// Example Usage:
type UserEvents = EventName<'user'>; // 'userChanged' | 'userDeleted'
type ProductEvents = EventName<'product'>; // 'productChanged' | 'productDeleted'

type PropSetter<T extends string> = `set${Capitalize<T>}`;

type SetUserName = PropSetter<'userName'>; // 'setUserName'

These types are excellent for defining strict event names, API endpoints, or property setters/getters based on base strings, ensuring compile-time safety for string-based operations. Capitalize<T> is a built-in utility type that transforms the first letter of a string literal to uppercase.

4. Variadic Tuple Types

Variadic tuple types allow generics to operate over arrays/tuples of arbitrary length, supporting the use of spread syntax (...) to infer and construct tuple types. This is particularly useful for functions that accept a variable number of arguments and need to retain type information about each argument.

typescript
type Concat<T extends any[], U extends any[]> = [...T, ...U];

// Example Usage:
type CombinedTuple = Concat<[1, 2], ['a', 'b']>; // [1, 2, 'a', 'b']

type Head<T extends any[]> = T extends [infer H, ...any[]] ? H : never;
type Tail<T extends any[]> = T extends [any, ...infer Rest] ? Rest : never;

type FirstElement = Head<[1, 'hello', true]>; // 1
type RemainingElements = Tail<[1, 'hello', true]>; // ['hello', true]

Concat demonstrates combining two generic tuple types into a new one. Head and Tail use infer with variadic tuple types to extract the first element and the rest of the elements, respectively. This enables precise type manipulation for array-like structures.

5. Recursive Conditional Types

Recursive conditional types are a powerful pattern where a conditional type refers to itself. This allows for processing deeply nested types, such as flattening arrays, unwrapping nested promises, or creating deeply immutable/optional types. They typically involve infer to peel off layers of a type structure.

typescript
type DeepFlatten<T> = T extends Array<infer U> ? DeepFlatten<U> : T;

// Example Usage:
type NestedArray = string[][][];
type FlatString = DeepFlatten<NestedArray>; // string

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

interface Config {
  theme: { primary: string; secondary: string; };
  settings: { timeout: number; debug: boolean; };
}

type PartialConfig = DeepPartial<Config>;
/*
{
  theme?: {
    primary?: string | undefined;
    secondary?: string | undefined;
  } | undefined;
  settings?: {
    timeout?: number | undefined;
    debug?: boolean | undefined;
  } | undefined;
}
*/

DeepFlatten recursively unwraps array types until it reaches a non-array type. DeepPartial combines mapped types with recursive conditional types to make all properties (and their nested properties) optional. This pattern is fundamental for advanced type transformations that operate across arbitrary depths of a type structure.

Conclusion

These advanced generic patterns are the backbone of many powerful TypeScript features and libraries. Mastering them allows you to write highly expressive and robust type definitions, significantly improving the type safety and maintainability of complex applications. While they can appear daunting initially, understanding their individual mechanics and how they compose will unlock new levels of type-level programming.