If TypeScript’s type system is a programming language, then conditional and mapped types are its control flow and loops. They let you transform types, create new types based on existing ones, and build incredibly sophisticated type-level logic. These are the tools that make libraries like React Hook Form and tRPC possible. Let’s master them.
Conditional Types: If-Then-Else for Types
Conditional types follow the pattern T extends U ? X : Y:
type IsString<T> = T extends string ? true : false;
type A = IsString<string>; // true
type B = IsString<number>; // false
type C = IsString<'hello'>; // true (string literal extends string)
// More practical example
type Flatten<T> = T extends Array<infer Item> ? Item : T;
type Str = Flatten<string>; // string
type Num = Flatten<number[]>; // number
type Mixed = Flatten<(string | number)[]>; // string | numberThe infer Keyword
infer lets you extract types from other types:
// Extract return type
type ReturnType<T> = T extends (...args: any[]) => infer R ? R : never;
function getString(): string {
return 'hello';
}
type StringReturn = ReturnType<typeof getString>; // string
// Extract promise type
type UnwrapPromise<T> = T extends Promise<infer U> ? U : T;
type PromiseString = UnwrapPromise<Promise<string>>; // string
type JustString = UnwrapPromise<string>; // string
// Extract array element type
type ElementType<T> = T extends (infer E)[] ? E : never;
type StringElement = ElementType<string[]>; // string
type NumberElement = ElementType<number[]>; // numberReact Component Props Extraction
// Extract props type from a component
type ComponentProps<T> = T extends React.ComponentType<infer P> ? P : never;
const Button = (props: { label: string; onClick: () => void }) => {
return <button onClick={props.onClick}>{props.label}</button>;
};
type ButtonProps = ComponentProps<typeof Button>;
// { label: string; onClick: () => void }
// Extract props from different component types
type PropsOf<C> = C extends React.ComponentType<infer P>
? P
: C extends keyof JSX.IntrinsicElements
? JSX.IntrinsicElements[C]
: never;
type DivProps = PropsOf<'div'>; // React.HTMLAttributes<HTMLDivElement>
type CustomProps = PropsOf<typeof Button>; // Button's propsMapped Types: Transforming Object Types
Mapped types let you create new types by transforming properties of existing types:
// Basic mapped type
type Readonly<T> = {
readonly [P in keyof T]: T[P];
};
interface User {
name: string;
age: number;
}
type ReadonlyUser = Readonly<User>;
// { readonly name: string; readonly age: number; }
// Make all properties optional
type Partial<T> = {
[P in keyof T]?: T[P];
};
type PartialUser = Partial<User>;
// { name?: string; age?: number; }
// Make all properties required
type Required<T> = {
[P in keyof T]-?: T[P]; // -? removes optional
};Key Remapping with Template Literals
// Getters and setters
type Getters<T> = {
[K in keyof T as `get${Capitalize<string & K>}`]: () => T[K];
};
type Setters<T> = {
[K in keyof T as `set${Capitalize<string & K>}`]: (value: T[K]) => void;
};
interface State {
name: string;
age: number;
isActive: boolean;
}
type StateGetters = Getters<State>;
// {
// getName: () => string;
// getAge: () => number;
// getIsActive: () => boolean;
// }
type StateSetters = Setters<State>;
// {
// setName: (value: string) => void;
// setAge: (value: number) => void;
// setIsActive: (value: boolean) => void;
// }Filtering Properties
// Pick only string properties
type StringProperties<T> = {
[K in keyof T as T[K] extends string ? K : never]: T[K];
};
interface Mixed {
id: number;
name: string;
email: string;
age: number;
isActive: boolean;
}
type OnlyStrings = StringProperties<Mixed>;
// { name: string; email: string; }
// Pick methods only
type Methods<T> = {
[K in keyof T as T[K] extends Function ? K : never]: T[K];
};
interface Service {
url: string;
timeout: number;
get(): Promise<any>;
post(data: any): Promise<any>;
}
type ServiceMethods = Methods<Service>;
// { get(): Promise<any>; post(data: any): Promise<any>; }Conditional Types with Mapped Types
// Make specific properties optional
type PartialBy<T, K extends keyof T> = Omit<T, K> & Partial<Pick<T, K>>;
interface User {
id: string;
name: string;
email: string;
password: string;
}
type UserUpdate = PartialBy<User, 'email' | 'password'>;
// { id: string; name: string; email?: string; password?: string; }
// Nullable properties
type Nullable<T> = {
[P in keyof T]: T[P] | null;
};
type NullableUser = Nullable<User>;
// All properties can be null
// Deep partial
type DeepPartial<T> = {
[P in keyof T]?: T[P] extends object ? DeepPartial<T[P]> : T[P];
};
interface Config {
server: {
host: string;
port: number;
ssl: {
enabled: boolean;
cert: string;
};
};
}
type PartialConfig = DeepPartial<Config>;
// Everything is optional, including nested propertiesReact Form Types
// Form field state
type FieldState<T> = {
value: T;
error?: string;
touched: boolean;
};
// Convert model to form state
type FormState<T> = {
[K in keyof T]: FieldState<T[K]>;
};
interface LoginModel {
email: string;
password: string;
remember: boolean;
}
type LoginFormState = FormState<LoginModel>;
// {
// email: FieldState<string>;
// password: FieldState<string>;
// remember: FieldState<boolean>;
// }
// Form change handlers
type FormHandlers<T> = {
[K in keyof T as `onChange${Capitalize<string & K>}`]: (value: T[K]) => void;
};
type LoginHandlers = FormHandlers<LoginModel>;
// {
// onChangeEmail: (value: string) => void;
// onChangePassword: (value: string) => void;
// onChangeRemember: (value: boolean) => void;
// }Advanced Conditional Types
// Distribute over unions
type IsStringType<T> = T extends string ? true : false;
type TestUnion = IsStringType<string | number>; // boolean (true | false)
// Non-distributive (wrapped in tuple)
type IsStringTypeNonDist<T> = [T] extends [string] ? true : false;
type TestUnionNonDist = IsStringTypeNonDist<string | number>; // false
// Extract non-nullable
type NonNullable<T> = T extends null | undefined ? never : T;
type MaybeString = string | null | undefined;
type DefiniteString = NonNullable<MaybeString>; // string
// Function overloads
type Parameters<T> = T extends (...args: infer P) => any ? P : never;
function greet(name: string): string;
function greet(first: string, last: string): string;
function greet(...args: string[]): string {
return `Hello ${args.join(' ')}`;
}
type GreetParams = Parameters<typeof greet>;
// [name: string] | [first: string, last: string]Recursive Conditional Types
// Deeply nested promise unwrapping
type DeepAwait<T> = T extends Promise<infer U> ? DeepAwait<U> : T;
type Nested = Promise<Promise<Promise<string>>>;
type Unwrapped = DeepAwait<Nested>; // string
// Flatten nested arrays
type Flatten<T> = T extends readonly (infer U)[] ? Flatten<U> : T;
type NestedArray = number[][][];
type Flat = Flatten<NestedArray>; // number
// Path types for nested objects
type Path<T, K extends keyof T = keyof T> = K extends string
? T[K] extends object
? `${K}` | `${K}.${Path<T[K]>}`
: `${K}`
: never;
interface Person {
name: string;
address: {
street: string;
city: string;
country: {
code: string;
name: string;
};
};
}
type PersonPaths = Path<Person>;
// "name" | "address" | "address.street" | "address.city" |
// "address.country" | "address.country.code" | "address.country.name"Template for API Response Types
// API response wrapper
type ApiResponse<T, E = Error> = { success: true; data: T } | { success: false; error: E };
// Transform all methods to return API responses
type AsyncApi<T> = {
[K in keyof T]: T[K] extends (...args: infer A) => infer R
? (...args: A) => Promise<ApiResponse<Awaited<R>>>
: never;
};
interface UserService {
getUser(id: string): User;
updateUser(id: string, data: Partial<User>): User;
deleteUser(id: string): void;
}
type AsyncUserService = AsyncApi<UserService>;
// {
// getUser(id: string): Promise<ApiResponse<User>>;
// updateUser(id: string, data: Partial<User>): Promise<ApiResponse<User>>;
// deleteUser(id: string): Promise<ApiResponse<void>>;
// }React Props Manipulation
// Remove event handlers
type DataProps<T> = {
[K in keyof T as K extends `on${string}` ? never : K]: T[K];
};
interface ButtonProps {
label: string;
disabled: boolean;
onClick: () => void;
onHover: () => void;
}
type ButtonDataProps = DataProps<ButtonProps>;
// { label: string; disabled: boolean; }
// Make event handlers optional
type OptionalEvents<T> = {
[K in keyof T as K extends `on${string}` ? K : never]?: T[K];
} & {
[K in keyof T as K extends `on${string}` ? never : K]: T[K];
};
type FlexibleButton = OptionalEvents<ButtonProps>;
// { label: string; disabled: boolean; onClick?: () => void; onHover?: () => void; }Type-Safe Object Path Access
// Get type at path
type GetPath<T, Path extends string> = Path extends `${infer Key}.${infer Rest}`
? Key extends keyof T
? GetPath<T[Key], Rest>
: never
: Path extends keyof T
? T[Path]
: never;
interface Data {
user: {
profile: {
name: string;
age: number;
};
settings: {
theme: 'light' | 'dark';
};
};
}
type UserName = GetPath<Data, 'user.profile.name'>; // string
type Theme = GetPath<Data, 'user.settings.theme'>; // 'light' | 'dark'
// Safe get function
function get<T, P extends Path<T>>(obj: T, path: P): GetPath<T, P> {
const keys = path.split('.');
let result: any = obj;
for (const key of keys) {
result = result[key];
}
return result;
}Branded Types with Conditionals
// Create branded types
type Brand<T, B> = T & { __brand: B };
// Validation with conditional types
type ValidEmail<T> = T extends `${string}@${string}.${string}` ? Brand<T, 'Email'> : never;
type Email1 = ValidEmail<'user@example.com'>; // Branded as Email
type Email2 = ValidEmail<'invalid'>; // never
// Runtime validation that matches
function isValidEmail<T extends string>(email: T): email is ValidEmail<T> {
return /^[^\s@]+@[^\s@]+\.[^\s@]+$/.test(email);
}
const email = 'user@example.com';
if (isValidEmail(email)) {
// email is branded as ValidEmail
sendEmail(email);
}React Component Type Transformations
// HOC type transformation
type WithLoading<P> = P & {
loading?: boolean;
error?: Error;
};
// Transform component type
type WithLoadingComponent<C> =
C extends React.ComponentType<infer P> ? React.ComponentType<WithLoading<P>> : never;
// Inject props
type InjectProps<P, I> = Omit<P, keyof I> & Partial<I>;
// HOC that injects theme
interface ThemeProps {
theme: {
primaryColor: string;
secondaryColor: string;
};
}
type ThemedComponent<C> =
C extends React.ComponentType<infer P> ? React.ComponentType<InjectProps<P, ThemeProps>> : never;Utility Type Combinations
// Mutable (remove readonly)
type Mutable<T> = {
-readonly [P in keyof T]: T[P];
};
// Deep readonly
type DeepReadonly<T> = {
readonly [P in keyof T]: T[P] extends object ? DeepReadonly<T[P]> : T[P];
};
// Pick and make partial
type PickPartial<T, K extends keyof T> = Partial<Pick<T, K>>;
// Omit and make required
type OmitRequired<T, K extends keyof T> = Required<Omit<T, K>>;
// Exclusive OR (XOR)
type XOR<T, U> =
| (T & { [K in Exclude<keyof U, keyof T>]?: never })
| (U & { [K in Exclude<keyof T, keyof U>]?: never });
interface Name {
name: string;
}
interface Id {
id: number;
}
// Can have name OR id, but not both
type Identifier = XOR<Name, Id>;
const id1: Identifier = { name: 'Alice' }; // ✅
const id2: Identifier = { id: 123 }; // ✅
// const id3: Identifier = { name: 'Alice', id: 123 }; // ❌ ErrorReal-World Example: Type-Safe API Client
// API definition
interface APIEndpoints {
'/users': {
GET: { response: User[]; params: { page?: number } };
POST: { response: User; body: CreateUserDTO };
};
'/users/:id': {
GET: { response: User };
PUT: { response: User; body: UpdateUserDTO };
DELETE: { response: void };
};
}
// Extract method types
type HTTPMethod = 'GET' | 'POST' | 'PUT' | 'DELETE';
// Type-safe client
type APIClient = {
[Path in keyof APIEndpoints]: {
[Method in HTTPMethod as Lowercase<Method>]: Method extends keyof APIEndpoints[Path]
? APIEndpoints[Path][Method] extends { body: infer B; response: infer R }
? (body: B) => Promise<R>
: APIEndpoints[Path][Method] extends { params: infer P; response: infer R }
? (params?: P) => Promise<R>
: APIEndpoints[Path][Method] extends { response: infer R }
? () => Promise<R>
: never
: never;
};
};
// Usage would be fully typed:
// client['/users'].get({ page: 1 })
// client['/users'].post({ name: 'Alice', email: 'alice@example.com' })
// client['/users/:id'].delete()Performance Tips
Avoid Deep Recursion
// ❌ Can hit recursion limit
type DeepKeys<T> = T extends object ? { [K in keyof T]: K | DeepKeys<T[K]> }[keyof T] : never;
// ✅ Limit recursion depth
type Keys<T, Depth extends number = 5> = [Depth] extends [0]
? never
: T extends object
? { [K in keyof T]: K | Keys<T[K], Prev[Depth]> }[keyof T]
: never;
type Prev = [never, 0, 1, 2, 3, 4];Use Type Aliases for Complex Types
// ✅ Good: Break down complex types
type IsFunction<T> = T extends Function ? true : false;
type FunctionKeys<T> = {
[K in keyof T]: IsFunction<T[K]> extends true ? K : never;
}[keyof T];
// ❌ Bad: Everything inline
type FunctionKeys<T> = {
[K in keyof T]: T[K] extends Function ? K : never;
}[keyof T];Best Practices
Start Simple
// Build up complexity gradually
type Simple<T> = T extends string ? true : false;
type Medium<T> = T extends `${infer Prefix}_${infer Suffix}` ? [Prefix, Suffix] : never;
type Complex<T> = T extends Record<infer K, infer V> ? /* ... */ : never;Document Complex Types
/**
* Recursively makes all properties optional
* @example
* type Result = DeepPartial<{ a: { b: string } }>
* // { a?: { b?: string } }
*/
type DeepPartial<T> = {
[P in keyof T]?: T[P] extends object ? DeepPartial<T[P]> : T[P];
};Test Your Types
// Use type assertions to test
type Assert<T extends true> = T;
type Test1 = Assert<IsString<string> extends true ? true : false>; // ✅
type Test2 = Assert<IsString<number> extends false ? true : false>; // ✅Summary
Conditional and mapped types are TypeScript’s power tools:
- Conditional Types - Type-level if-then-else logic
- Mapped Types - Transform object types systematically
inferKeyword - Extract types from complex structures- Key Remapping - Create new property names with template literals
- Recursive Types - Handle deeply nested structures
Master these, and you’ll be able to create type-safe abstractions that seemed impossible before!