Why Advanced TypeScript Types Matter
TypeScript's type system is Turing-complete — it can express nearly any constraint you can imagine. The advanced features covered here let you write APIs that are both flexible and precisely typed, catching entire classes of bugs at compile time rather than runtime.
Generics with Constraints
// Basic generic
function identity<T>(value: T): T {
return value;
}
// Constrained generic — T must have a length property
function longest<T extends { length: number }>(a: T, b: T): T {
return a.length >= b.length ? a : b;
}
longest("hello", "world"); // ✅ strings have .length
longest([1, 2, 3], [1, 2]); // ✅ arrays have .length
longest(1, 2); // ❌ Error: number has no .length
// Multiple type parameters
function zip<T, U>(arr1: T[], arr2: U[]): [T, U][] {
return arr1.map((item, i) => [item, arr2[i]]);
}
const pairs = zip([1, 2, 3], ['a', 'b', 'c']);
// pairs: [number, string][]
Conditional Types
Types that branch based on conditions:
// Basic conditional type
type IsString<T> = T extends string ? 'yes' : 'no';
type A = IsString<string>; // 'yes'
type B = IsString<number>; // 'no'
type C = IsString<string | number>; // 'yes' | 'no' (distributes!)
// Extract/Exclude (built-in conditionals)
type Exclude<T, U> = T extends U ? never : T;
type Extract<T, U> = T extends U ? T : never;
type Status = 'pending' | 'active' | 'deleted';
type ActiveStatus = Exclude<Status, 'deleted'>; // 'pending' | 'active'
// NonNullable
type NonNullable<T> = T extends null | undefined ? never : T;
type Defined = NonNullable<string | null | undefined>; // string
The infer Keyword
Extract type information from generic structures:
// Extract return type of a function
type ReturnType<T> = T extends (...args: any[]) => infer R ? R : never;
function fetchUser(): Promise<{ id: string; name: string }> { ... }
type UserResponse = ReturnType<typeof fetchUser>;
// UserResponse = Promise<{ id: string; name: string }>
// Unwrap Promise
type Awaited<T> = T extends Promise<infer U> ? Awaited<U> : T;
type User = Awaited<Promise<Promise<{ id: string }>>>;
// User = { id: string }
// Extract function parameter types
type Parameters<T> = T extends (...args: infer P) => any ? P : never;
function createUser(name: string, age: number, role: 'admin' | 'user') {}
type CreateUserParams = Parameters<typeof createUser>;
// [string, number, 'admin' | 'user']
Mapped Types
Transform object types systematically:
// Make all properties optional
type Partial<T> = { [K in keyof T]?: T[K] };
// Make all properties required
type Required<T> = { [K in keyof T]-?: T[K] };
// Make all properties readonly
type Readonly<T> = { readonly [K in keyof T]: T[K] };
// Custom mapping: make all nullable
type Nullable<T> = { [K in keyof T]: T[K] | null };
// Transform values
type Stringify<T> = { [K in keyof T]: string };
// Select subset of keys
type Pick<T, K extends keyof T> = { [P in K]: T[P] };
type Omit<T, K extends keyof T> = Pick<T, Exclude<keyof T, K>>;
interface User {
id: string;
name: string;
email: string;
password: string;
}
type PublicUser = Omit<User, 'password'>;
// { id: string; name: string; email: string }
type UserPreview = Pick<User, 'id' | 'name'>;
// { id: string; name: string }
Template Literal Types
Manipulate string types at the type level:
type EventName = 'click' | 'focus' | 'blur';
type EventHandler = `on${Capitalize<EventName>}`;
// 'onClick' | 'onFocus' | 'onBlur'
// API route types
type HttpMethod = 'get' | 'post' | 'put' | 'delete';
type Endpoint = '/users' | '/orders' | '/products';
type ApiRoute = `${Uppercase<HttpMethod>} ${Endpoint}`;
// 'GET /users' | 'GET /orders' | ... (all combinations)
// Type-safe CSS property names
type CSSProperty = 'margin' | 'padding' | 'border';
type CSSDirection = 'top' | 'right' | 'bottom' | 'left';
type DirectionalProperty = `${CSSProperty}-${CSSDirection}`;
// 'margin-top' | 'margin-right' | ... etc
// Object key inference
type Getters<T> = {
[K in keyof T as `get${Capitalize<string & K>}`]: () => T[K]
};
interface Config {
host: string;
port: number;
}
type ConfigGetters = Getters<Config>;
// { getHost: () => string; getPort: () => number }
Discriminated Unions
Pattern-match on type-safe variants:
type Result<T, E = Error> =
| { success: true; data: T }
| { success: false; error: E };
async function fetchUser(id: string): Promise<Result<User>> {
try {
const user = await db.users.findById(id);
return { success: true, data: user };
} catch (error) {
return { success: false, error: error as Error };
}
}
const result = await fetchUser('123');
if (result.success) {
console.log(result.data.name); // TypeScript knows data exists
} else {
console.error(result.error.message); // TypeScript knows error exists
}
// Exhaustive matching
type Shape =
| { kind: 'circle'; radius: number }
| { kind: 'square'; side: number }
| { kind: 'triangle'; base: number; height: number };
function area(shape: Shape): number {
switch (shape.kind) {
case 'circle': return Math.PI * shape.radius ** 2;
case 'square': return shape.side ** 2;
case 'triangle': return 0.5 * shape.base * shape.height;
default:
// This line will error if you add a new Shape variant without handling it
const _exhaustive: never = shape;
throw new Error(`Unhandled: ${_exhaustive}`);
}
}
Utility Type Patterns
// Deep partial (recursively make optional)
type DeepPartial<T> = {
[K in keyof T]?: T[K] extends object ? DeepPartial<T[K]> : T[K];
};
// Recursive readonly
type DeepReadonly<T> = {
readonly [K in keyof T]: T[K] extends object ? DeepReadonly<T[K]> : T[K];
};
// Flatten union to intersection
type UnionToIntersection<U> =
(U extends any ? (x: U) => void : never) extends (x: infer I) => void
? I
: never;
// Make specific keys required, rest optional
type RequiredKeys<T, K extends keyof T> = Omit<T, K> & Required<Pick<T, K>>;
interface CreateUserDto {
name?: string;
email?: string;
role?: string;
}
type ValidCreateUserDto = RequiredKeys<CreateUserDto, 'name' | 'email'>;
// { name: string; email: string; role?: string }
→ View and inspect complex TypeScript data structures with the JSON Viewer.