Building on the "Foundations of Advanced Generics: Parameters and Constraints" lesson, we now delve into powerful mechanisms for transforming existing types: Conditional and Mapped Types. These features enable you to derive new types based on the structure or properties of others, leading to incredibly flexible and robust type definitions.
infer KeywordConditional types provide a way to select one of two possible types based on a condition, using a syntax similar to JavaScript's ternary operator: T extends U ? X : Y. If type T is assignable to type U, the result is X; otherwise, it's Y. A key behavior is their distributivity over union types: if T is a union (e.g., A | B), the condition is applied to each member individually. To prevent this, wrap the type in a single-element tuple: [T] extends [U] ? X : Y.
Combined with the infer keyword, conditional types become type-extraction powerhouses. infer allows you to declare a new type variable within the extends clause, capturing a type from a specific position. For example, the built-in ReturnType<T> extracts the return type of a function. It's defined using infer to capture the function's return signature, enabling types like type MyNumberFunc = () => number; type MyReturnType = ReturnType<MyNumberFunc>; // MyReturnType is number.
Mapped types allow you to create new object types by transforming the properties of an existing object type. Their syntax, { [K in keyof T]: NewType }, iterates over each property K in the input type T and applies a transformation to its value type. Built-in utilities like Partial<T> (makes all properties optional) and Readonly<T> (makes all properties read-only) are classic examples.
A powerful feature is the as clause, which allows you to remap property keys. This enables renaming, filtering, or creating new keys based on the original ones. For instance, to transform an interface like { id: number; name: string; } into one where each property is a getter method (e.g., getId: () => number;), you'd use a mapped type with as:
type Getters<T> = {
[K in keyof T as `get${Capitalize<string & K>}`]: () => T[K];
}
interface User { id: number; name: string; }
type UserGetters = Getters<User>;
// UserGetters is { getId: () => number; getName: () => string; }
This flexibility allows for creating highly expressive and type-safe APIs. Mastering these type transformation techniques sets the stage for understanding practical generic patterns, common pitfalls, and real-world applications, which we'll explore in our next lesson.