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.