// T extends U ? X : Y
// If T is assignable to U, result is X, else Y
type IsString<T> = T extends string ? true : false;
type IsArray<T> = T extends any[] ? true : false;
type A = IsString<string>; // true
type B = IsString<number>; // false
type C = IsArray<string[]>; // true
type D = IsArray<string>; // false
// NonNullable — remove null/undefined
type NonNullable<T> = T extends null | undefined ? never : T;
type E = NonNullable<string | null | undefined>; // string
// Unwrap array elements
type ElementOf<T> = T extends (infer U)[] ? U : T;
type F = ElementOf<string[]>; // string
type G = ElementOf<number[]>; // number
type H = ElementOf<string>; // string (not array)
// ReturnType — extract function return type
type ReturnType<T> = T extends (...args: any[]) => infer R ? R : never;
function greet(): string { return "Hello"; }
type GreetReturn = ReturnType<typeof greet>; // string
// Parameters — extract function params
type Parameters<T> = T extends (...args: infer P) => any ? P : never;
type GreetParams = Parameters<(name: string, age: number) => void>; // [string, number]
2. Mapped Types
// Transform every key in an object type
type Readonly<T> = {
readonly [K in keyof T]: T[K];
};
type Nullable<T> = {
[K in keyof T]: T[K] | null;
};
type Optional<T> = {
[K in keyof T]?: T[K];
};
// Remove readonly
type Mutable<T> = {
-readonly [K in keyof T]: T[K];
};
// Remove optional
type Required<T> = {
[K in keyof T]-?: T[K];
};
interface User {
id: number;
name: string;
email: string;
}
type ReadonlyUser = Readonly<User>; // all readonly
type NullableUser = Nullable<User>; // all can be null
type PartialUser = Optional<User>; // all optional
// Filter keys by value type
type FilterByType<T, U> = {
[K in keyof T as T[K] extends U ? K : never]: T[K];
};
interface Mixed {
id: number;
name: string;
active: boolean;
count: number;
}
type OnlyStrings = FilterByType<Mixed, string>; // { name: string }
type OnlyNumbers = FilterByType<Mixed, number>; // { id: number; count: number }
3. Template Literal Types
// String literal manipulation at type level
type EventName = 'click' | 'focus' | 'blur';
// Prefix with "on"
type EventHandler = `on${Capitalize<EventName>}`;
// "onClick" | "onFocus" | "onBlur"
// CSS property builder
type Side = 'top' | 'right' | 'bottom' | 'left';
type Spacing = `margin-${Side}` | `padding-${Side}`;
// "margin-top" | "margin-right" | ... | "padding-top" | ...
// API endpoint builder
type Method = 'get' | 'post' | 'put' | 'delete';
type Endpoint = '/users' | '/posts' | '/comments';
type Route = `${Uppercase<Method>} ${Endpoint}`;
// "GET /users" | "POST /users" | "GET /posts" | ...
// Getter/Setter type generator
type Getter<T, K extends keyof T & string> = {
[P in K as `get${Capitalize<P>}`]: () => T[P];
};
type Setter<T, K extends keyof T & string> = {
[P in K as `set${Capitalize<P>}`]: (value: T[P]) => void;
};
interface UserProps { name: string; age: number; }
type UserGetters = Getter<UserProps, keyof UserProps & string>;
// { getName: () => string; getAge: () => number; }
type UserSetters = Setter<UserProps, keyof UserProps & string>;
// { setName: (v: string) => void; setAge: (v: number) => void; }
4. Discriminated Unions
// Discriminated Union = union with common literal type field
// TypeScript can narrow type based on that field
type LoadingState = {
status: 'loading';
};
type SuccessState<T> = {
status: 'success';
data: T;
};
type ErrorState = {
status: 'error';
error: string;
code: number;
};
type AsyncState<T> = LoadingState | SuccessState<T> | ErrorState;
// Type narrowing via switch
function renderUser(state: AsyncState<User>): string {
switch (state.status) {
case 'loading':
return '⏳ Loading...';
case 'success':
return `✅ ${state.data.name}`; // TS knows: state.data exists
case 'error':
return `❌ Error ${state.code}: ${state.error}`; // state.error exists
default:
const _exhaustive: never = state; // exhaustiveness check!
return _exhaustive;
}
}
// Shape union example
type Shape =
| { kind: 'circle'; radius: number }
| { kind: 'rectangle'; width: number; height: number }
| { kind: 'triangle'; base: number; height: number };
function area(shape: Shape): number {
switch (shape.kind) {
case 'circle': return Math.PI * shape.radius ** 2;
case 'rectangle': return shape.width * shape.height;
case 'triangle': return 0.5 * shape.base * shape.height;
}
}
5. infer — Type Inference
// infer = extract type from within a conditional type
// Unwrap Promise
type Awaited<T> = T extends Promise<infer R> ? R : T;
type A = Awaited<Promise<string>>; // string
type B = Awaited<Promise<number>>; // number
type C = Awaited<string>; // string (not promise)
// Unwrap array
type UnwrapArray<T> = T extends Array<infer U> ? U : T;
type D = UnwrapArray<string[]>; // string
type E = UnwrapArray<User[]>; // User
// Extract function params and return
type FnParams<T> = T extends (...args: infer P) => any ? P : never;
type FnReturn<T> = T extends (...args: any[]) => infer R ? R : never;
type FnAsync<T> = T extends (...args: any[]) => Promise<infer R> ? R : never;
async function fetchUser(id: number): Promise<User> { /* ... */ return {} as User; }
type FetchParams = FnParams<typeof fetchUser>; // [id: number]
type FetchReturn = FnReturn<typeof fetchUser>; // Promise<User>
type FetchData = FnAsync<typeof fetchUser>; // User (unwrapped!)
💡 infer is what powers built-in utility types: ReturnType, Parameters, Awaited, InstanceType