TypeScript

Advanced Generics

35 min Lesson 12 of 40

Advanced Generic Patterns

Now that you understand generic fundamentals, let's explore advanced generic patterns that leverage TypeScript's powerful type system. These patterns enable sophisticated type manipulations that make your code more expressive and maintainable.

Conditional Types

Conditional types allow you to select different types based on a condition. They follow the ternary operator pattern: T extends U ? X : Y

// Basic conditional type type IsString<T> = T extends string ? true : false; type Test1 = IsString<string>; // true type Test2 = IsString<number>; // false // Conditional type for extracting return types type ReturnType<T> = T extends (...args: any[]) => infer R ? R : never; function getUser(): { name: string; age: number } { return { name: "Alice", age: 30 }; } type UserType = ReturnType<typeof getUser>; // { name: string; age: number } // Conditional type for array elements type ElementType<T> = T extends (infer E)[] ? E : T; type StringArray = ElementType<string[]>; // string type NumberType = ElementType<number>; // number // Nested conditional types type TypeName<T> = T extends string ? "string" : T extends number ? "number" : T extends boolean ? "boolean" : T extends undefined ? "undefined" : T extends Function ? "function" : "object"; type T1 = TypeName<string>; // "string" type T2 = TypeName<() => void>; // "function"
Note: Conditional types are particularly powerful when combined with the infer keyword, which allows you to extract types from other types during the type-checking process.

The infer Keyword

The infer keyword lets you extract and name a type within a conditional type. Think of it as pattern matching for types:

// Extract return type from function type GetReturnType<T> = T extends (...args: any[]) => infer R ? R : never; function add(a: number, b: number): number { return a + b; } type AddReturn = GetReturnType<typeof add>; // number // Extract parameter types type GetFirstParam<T> = T extends (first: infer F, ...args: any[]) => any ? F : never; type FirstParam = GetFirstParam<typeof add>; // number // Extract all parameters as tuple type Parameters<T> = T extends (...args: infer P) => any ? P : never; type AddParams = Parameters<typeof add>; // [a: number, b: number] // Extract element type from Promise type UnwrapPromise<T> = T extends Promise<infer U> ? U : T; type AsyncUser = UnwrapPromise<Promise<{ name: string }>>; // { name: string } type SyncUser = UnwrapPromise<{ name: string }>; // { name: string } // Deep promise unwrapping type DeepUnwrap<T> = T extends Promise<infer U> ? DeepUnwrap<U> : T; type Nested = DeepUnwrap<Promise<Promise<Promise<string>>>>; // string // Extract instance type from constructor type InstanceType<T> = T extends new (...args: any[]) => infer R ? R : never; class User { name: string; constructor(name: string) { this.name = name; } } type UserInstance = InstanceType<typeof User>; // User

Distributive Conditional Types

When conditional types act on union types, they distribute over each member of the union:

// Distributive conditional type type ToArray<T> = T extends any ? T[] : never; type StringOrNumber = string | number; type Arrays = ToArray<StringOrNumber>; // string[] | number[] // Non-distributive version (using tuple trick) type ToArrayNonDist<T> = [T] extends [any] ? T[] : never; type SingleArray = ToArrayNonDist<StringOrNumber>; // (string | number)[] // Filter null and undefined from union type NonNullable<T> = T extends null | undefined ? never : T; type MaybeString = string | null | undefined; type DefiniteString = NonNullable<MaybeString>; // string // Extract specific types from union type ExtractStrings<T> = T extends string ? T : never; type Mixed = string | number | boolean | string[]; type OnlyStrings = ExtractStrings<Mixed>; // string // Exclude specific types from union type ExcludeNumbers<T> = T extends number ? never : T; type WithoutNumbers = ExcludeNumbers<Mixed>; // string | boolean | string[]
Tip: To prevent distribution, wrap the type parameter in a tuple: [T] extends [U] instead of T extends U. This is useful when you want to treat union types as a single unit.

Mapped Types

Mapped types transform each property in a type. They're perfect for creating variations of existing types:

// Basic mapped type - make all properties optional type Partial<T> = { [P in keyof T]?: T[P]; }; interface User { id: number; name: string; email: string; } type PartialUser = Partial<User>; // { id?: number; name?: string; email?: string } // Make all properties required type Required<T> = { [P in keyof T]-?: T[P]; }; type RequiredUser = Required<PartialUser>; // { id: number; name: string; email: string } // Make all properties readonly type Readonly<T> = { readonly [P in keyof T]: T[P]; }; type ReadonlyUser = Readonly<User>; // { readonly id: number; readonly name: string; readonly email: string } // Pick specific properties type Pick<T, K extends keyof T> = { [P in K]: T[P]; }; type UserNameAndEmail = Pick<User, "name" | "email">; // { name: string; email: string } // Omit specific properties type Omit<T, K extends keyof T> = Pick<T, Exclude<keyof T, K>>; type UserWithoutId = Omit<User, "id">; // { name: string; email: string } // Create a record type type Record<K extends keyof any, T> = { [P in K]: T; }; type UserRoles = Record<"admin" | "user" | "guest", boolean>; // { admin: boolean; user: boolean; guest: boolean }

Advanced Mapped Type Patterns

Combine mapped types with conditional types for powerful transformations:

// Make properties nullable type Nullable<T> = { [P in keyof T]: T[P] | null; }; type NullableUser = Nullable<User>; // { id: number | null; name: string | null; email: string | null } // Convert function properties to their return types type FunctionPropertyTypes<T> = { [P in keyof T]: T[P] extends Function ? ReturnType<T[P]> : T[P]; }; interface Actions { getName: () => string; getAge: () => number; isActive: boolean; } type ActionResults = FunctionPropertyTypes<Actions>; // { getName: string; getAge: number; isActive: boolean } // Deep readonly type DeepReadonly<T> = { readonly [P in keyof T]: T[P] extends object ? DeepReadonly<T[P]> : T[P]; }; interface Company { name: string; address: { street: string; city: string; }; } type ReadonlyCompany = DeepReadonly<Company>; // { // readonly name: string; // readonly address: { // readonly street: string; // readonly city: string; // } // } // Getters type - convert properties to getter functions type Getters<T> = { [P in keyof T as `get${Capitalize<string & P>}`]: () => T[P]; }; type UserGetters = Getters<User>; // { // getId: () => number; // getName: () => string; // getEmail: () => string; // }
Warning: Deep recursive types can cause TypeScript to hit recursion limits. For deeply nested structures, consider setting a maximum depth or using the -? modifier to handle optional properties carefully.

Template Literal Types

Template literal types allow you to manipulate string literal types using template syntax:

// Basic template literal type type Greeting<T extends string> = `Hello ${T}`; type HelloWorld = Greeting<"World">; // "Hello World" // Event naming pattern type EventName<T extends string> = `on${Capitalize<T>}`; type ClickEvent = EventName<"click">; // "onClick" type ChangeEvent = EventName<"change">; // "onChange" // Generate multiple event handlers type Events = "click" | "hover" | "focus"; type EventHandlers = { [E in Events as `on${Capitalize<E>}`]: (event: Event) => void; }; // { // onClick: (event: Event) => void; // onHover: (event: Event) => void; // onFocus: (event: Event) => void; // } // CSS property naming type CSSProperty<T extends string> = `--${T}`; type ThemeColors = "primary" | "secondary" | "accent"; type CSSVariables = { [C in ThemeColors as CSSProperty<C>]: string; }; // { // "--primary": string; // "--secondary": string; // "--accent": string; // } // Combine multiple string literals type HTTPMethod = "GET" | "POST" | "PUT" | "DELETE"; type Endpoint = "users" | "posts" | "comments"; type APIRoute = `/${Endpoint}`; type APICall = `${HTTPMethod} ${APIRoute}`; type UserRoutes = Extract<APICall, `${string}users`>; // "GET /users" | "POST /users" | "PUT /users" | "DELETE /users" // Pattern matching with template literals type ExtractParam<T extends string> = T extends `${string}:${infer Param}/${infer Rest}` ? Param | ExtractParam<`/${Rest}`> : T extends `${string}:${infer Param}` ? Param : never; type Route = "/users/:userId/posts/:postId"; type Params = ExtractParam<Route>; // "userId" | "postId"

Key Remapping in Mapped Types

TypeScript allows you to remap keys while creating mapped types using the as clause:

// Remove specific properties by remapping to never type RemoveKindField<T> = { [P in keyof T as Exclude<P, "kind">]: T[P]; }; interface Circle { kind: "circle"; radius: number; } type CircleWithoutKind = RemoveKindField<Circle>; // { radius: number } // Prefix property names type Prefixed<T, P extends string> = { [K in keyof T as `${P}${Capitalize<string & K>}`]: T[K]; }; type PrefixedUser = Prefixed<User, "user">; // { userId: number; userName: string; userEmail: string } // Filter properties by type type PickByType<T, U> = { [P in keyof T as T[P] extends U ? P : never]: T[P]; }; interface Mixed { name: string; age: number; active: boolean; email: string; } type StringProps = PickByType<Mixed, string>; // { name: string; email: string } type NumberProps = PickByType<Mixed, number>; // { age: number } // Create discriminated union from properties type Discriminate<T, K extends keyof T> = { [P in keyof T]: { type: P; value: T[P] }; }[keyof T]; type Config = { string: string; number: number; boolean: boolean; }; type ConfigValue = Discriminate<Config, "string" | "number" | "boolean">; // { type: "string"; value: string } | // { type: "number"; value: number } | // { type: "boolean"; value: boolean }

Recursive Conditional Types

TypeScript supports recursive type definitions, enabling complex type transformations:

// Recursive type for flattening nested arrays type Flatten<T> = T extends Array<infer U> ? U extends Array<any> ? Flatten<U> : U : T; type Nested = [[[string]], number]; type Flat = Flatten<Nested>; // string | number // Recursive type for deep partial type DeepPartial<T> = { [P in keyof T]?: T[P] extends object ? DeepPartial<T[P]> : T[P]; }; interface NestedConfig { database: { host: string; port: number; credentials: { username: string; password: string; }; }; cache: { enabled: boolean; ttl: number; }; } type PartialConfig = DeepPartial<NestedConfig>; // All properties at all levels are optional // Recursive type for path strings type PathKeys<T, Prefix extends string = ""> = { [K in keyof T]: T[K] extends object ? K extends string ? `${Prefix}${K}` | PathKeys<T[K], `${Prefix}${K}.`> : never : K extends string ? `${Prefix}${K}` : never; }[keyof T]; type ConfigPaths = PathKeys<NestedConfig>; // "database" | "database.host" | "database.port" | // "database.credentials" | "database.credentials.username" | ... // Get value type by path type PathValue<T, P extends string> = P extends `${infer K}.${infer Rest}` ? K extends keyof T ? PathValue<T[K], Rest> : never : P extends keyof T ? T[P] : never; type HostType = PathValue<NestedConfig, "database.host">; // string type PortType = PathValue<NestedConfig, "database.port">; // number
Note: TypeScript 4.5+ increased the recursion limit for conditional types to 1000, but be mindful of performance implications with deeply recursive types. Use them judiciously in production code.

Practical Advanced Generic Patterns

Let's combine these concepts into real-world utility types:

// Type-safe event emitter type EventMap = { [event: string]: any; }; class TypedEventEmitter<T extends EventMap> { private listeners: { [K in keyof T]?: Array<(data: T[K]) => void> } = {}; on<K extends keyof T>(event: K, listener: (data: T[K]) => void): void { if (!this.listeners[event]) { this.listeners[event] = []; } this.listeners[event]!.push(listener); } emit<K extends keyof T>(event: K, data: T[K]): void { this.listeners[event]?.forEach(listener => listener(data)); } } // Usage interface MyEvents { login: { userId: number; timestamp: Date }; logout: { userId: number }; error: { message: string; code: number }; } const emitter = new TypedEventEmitter<MyEvents>(); emitter.on("login", (data) => { console.log(data.userId); // Type-safe: userId is number console.log(data.timestamp); // Type-safe: timestamp is Date }); emitter.emit("login", { userId: 1, timestamp: new Date() }); // OK // emitter.emit("login", { userId: "1" }); // Error: wrong type // Builder pattern with type accumulation type Builder<T> = { [K in keyof T]-?: (value: T[K]) => Builder<T> & { build(): T }; }; function createBuilder<T>(): Builder<T> { const data: any = {}; const builder: any = {}; return new Proxy(builder, { get(_, prop) { if (prop === "build") { return () => data; } return (value: any) => { data[prop] = value; return builder; }; } }); } interface Person { name: string; age: number; email: string; } const person = createBuilder<Person>() .name("Alice") .age(30) .email("alice@example.com") .build(); // Type: Person
Exercise: Create a type-safe query builder that constructs SQL-like queries. Define a Query<T> type with methods select<K extends keyof T>(...keys: K[]), where(condition: Partial<T>), and orderBy<K extends keyof T>(key: K, direction: "asc" | "desc"). Use conditional types to track which methods have been called and prevent invalid query construction.

Summary

Advanced generics unlock TypeScript's full type system capabilities. You've learned conditional types for type-level logic, the infer keyword for type extraction, mapped types for transforming object types, template literal types for string manipulation, and recursive types for deep transformations. These patterns are the foundation of sophisticated TypeScript libraries and frameworks, enabling exceptional type safety and developer experience.