Advanced Types
TypeScript Programming
Advanced Types
TypeScript's type system is Turing-complete, meaning it can express arbitrarily complex type transformations. Advanced types — including mapped types, conditional types, template literal types, and the infer keyword — allow you to write type-level programs that compute new types from existing ones. These features power the utility types we covered earlier and enable library authors to provide extraordinarily precise type definitions that adapt to how their APIs are used. While these concepts are advanced, understanding them unlocks the full power of TypeScript.
Mapped Types
Mapped types create new object types by transforming each property of an existing type. They iterate over keys using the in keyword and apply modifications to each property's type or modifiers.
// Make all properties optional (this is how Partial<T> works internally)
type MyPartial<T> = {
[P in keyof T]?: T[P];
};
// Make all properties readonly
type MyReadonly<T> = {
readonly [P in keyof T]: T[P];
};
// Make all properties nullable
type Nullable<T> = {
[P in keyof T]: T[P] | null;
};
interface Config {
host: string;
port: number;
debug: boolean;
}
type NullableConfig = Nullable<Config>;
// { host: string | null; port: number | null; debug: boolean | null }
// Mapped type with key remapping (TypeScript 4.1+)
type Getters<T> = {
[P in keyof T as `get${Capitalize<string & P>}`]: () => T[P];
};
type ConfigGetters = Getters<Config>;
// { getHost: () => string; getPort: () => number; getDebug: () => boolean }
Conditional Types
Conditional types select one of two types based on a condition, using syntax similar to the ternary operator: T extends U ? X : Y. They are the foundation of type-level programming in TypeScript.
// Basic conditional type
type IsString<T> = T extends string ? "yes" : "no";
type A = IsString<string>; // "yes"
type B = IsString<number>; // "no"
// Extract array element type
type ElementOf<T> = T extends (infer E)[] ? E : never;
type Nums = ElementOf<number[]>; // number
type Strs = ElementOf<string[]>; // string
type Nope = ElementOf<boolean>; // never
// Extract function return type (how ReturnType<T> works)
type MyReturnType<T> = T extends (...args: any[]) => infer R ? R : never;
function fetchData(): Promise<string> {
return Promise.resolve("data");
}
type Result = MyReturnType<typeof fetchData>; // Promise<string>
// Distributive conditional types
type ToArray<T> = T extends any ? T[] : never;
type Distributed = ToArray<string | number>;
// string[] | number[] (distributes over each union member)
Template Literal Types
Template literal types combine literal types using template string syntax, creating new string literal types dynamically. Combined with mapped types, they enable powerful API patterns.
// Basic template literal type
type Greeting = `Hello, ${string}!`;
const g1: Greeting = "Hello, World!"; // OK
// const g2: Greeting = "Hi, World!"; // Error
// Event handler pattern
type EventName = "click" | "focus" | "blur";
type EventHandler = `on${Capitalize<EventName>}`;
// "onClick" | "onFocus" | "onBlur"
// CSS property builder
type CSSUnit = "px" | "em" | "rem" | "%";
type CSSValue = `${number}${CSSUnit}`;
const width: CSSValue = "100px"; // OK
const margin: CSSValue = "1.5rem"; // OK
// const bad: CSSValue = "100"; // Error
// Generate getter/setter method names from properties
type PropMethods<T> = {
[K in keyof T as `get${Capitalize<string & K>}`]: () => T[K];
} & {
[K in keyof T as `set${Capitalize<string & K>}`]: (value: T[K]) => void;
};
interface User { name: string; age: number; }
type UserMethods = PropMethods<User>;
// { getName: () => string; getAge: () => number;
// setName: (value: string) => void; setAge: (value: number) => void }
The infer Keyword
The infer keyword declares a type variable within a conditional type's extends clause, allowing you to extract and capture parts of complex types.
// Extract the resolved type from a Promise
type Awaited_<T> = T extends Promise<infer U> ? Awaited_<U> : T;
type R1 = Awaited_<Promise<string>>; // string
type R2 = Awaited_<Promise<Promise<number>>>; // number (recursive!)
// Extract function parameters
type FirstParam<T> = T extends (first: infer P, ...args: any[]) => any ? P : never;
type FP = FirstParam<(name: string, age: number) => void>; // string
// Extract constructor parameter types
type ConstructorParams<T> = T extends new (...args: infer P) => any ? P : never;
class MyService {
constructor(public name: string, public port: number) {}
}
type Params = ConstructorParams<typeof MyService>; // [string, number]
Tip: Advanced types are primarily used by library authors and in complex generic utilities. Application code rarely needs conditional types or mapped types directly, but understanding them helps you read and debug complex type errors from third-party libraries.
Key Takeaways
- Mapped types transform every property of an existing type using
[P in keyof T]syntax. - Conditional types select between two types based on an
extendscondition. - Template literal types create new string literal types using template syntax.
- The
inferkeyword extracts type parts within conditional types. - These features combine to enable type-level programming for precise, adaptive APIs.