TypeScript Advanced Patterns: Utility Types and Mapped Types
Master advanced TypeScript features including utility types, mapped types, conditional types, and template literal types for type-safe development.
Table of Contents
TypeScript Advanced Patterns: Utility Types and Mapped Types
TypeScript's advanced type system provides powerful tools for creating type-safe, maintainable code. Understanding utility types and mapped types is essential for advanced TypeScript development.
Utility Types
Partial
Makes all properties of T optional:
interface User {
id: number;
name: string;
email: string;
role: "admin" | "user";
}
type PartialUser = Partial<User>;
// Equivalent to:
// {
// id?: number;
// name?: string;
// email?: string;
// role?: 'admin' | 'user';
// }
function updateUser(id: number, updates: PartialUser): User {
// Implementation
}Required
Makes all properties of T required:
type StrictUser = Required<PartialUser>;
// All properties are now required againPick<T, K>
Creates a type by picking specific properties from T:
type UserCredentials = Pick<User, "email" | "role">;
// { email: string; role: 'admin' | 'user'; }
type PublicUser = Pick<User, "id" | "name">;
// { id: number; name: string; }Omit<T, K>
Creates a type by omitting specific properties from T:
type UserWithoutId = Omit<User, "id">;
// { name: string; email: string; role: 'admin' | 'user'; }Record<K, T>
Creates an object type with keys of type K and values of type T:
type UserRoles = Record<string, "admin" | "user" | "moderator">;
const roles: UserRoles = {
"alice": "admin",
"bob": "user",
"charlie": "moderator",
};Mapped Types
Basic Mapped Types
Transform existing types:
type ReadonlyUser = {
readonly [P in keyof User]: User[P];
};
type OptionalUser = {
[P in keyof User]?: User[P];
};Advanced Mapped Types
Create complex type transformations:
type Nullable<T> = {
[P in keyof T]: T[P] | null;
};
type DeepReadonly<T> = {
readonly [P in keyof T]: T[P] extends object ? DeepReadonly<T[P]>
: T[P];
};Template Literal Types
Use template literals in types:
type EventName = "click" | "hover" | "focus";
type EventHandler<T extends EventName> = `on${Capitalize<T>}`;
type Handlers = {
[K in EventName as EventHandler<K>]: () => void;
};
// {
// onClick: () => void;
// onHover: () => void;
// onFocus: () => void;
// }Conditional Types
Basic Conditional Types
Types that depend on conditions:
type IsString<T> = T extends string ? "yes" : "no";
type A = IsString<string>; // 'yes'
type B = IsString<number>; // 'no'Advanced Conditional Types
type Flatten<T> = T extends Array<infer U> ? U : T;
type A = Flatten<string[]>; // string
type B = Flatten<number>; // number
type FunctionReturnType<T> = T extends (...args: any[]) => infer R ? R : never;
type ReturnTypeOfParseInt = FunctionReturnType<typeof parseInt>; // numberDistributive Conditional Types
type ToArray<T> = T extends any ? T[] : never;
type A = ToArray<string | number>; // string[] | number[]Advanced Patterns
Branded Types
Create nominal types for better type safety:
type UserId = string & { readonly __brand: "UserId" };
type PostId = string & { readonly __brand: "PostId" };
function createUserId(id: string): UserId {
return id as UserId;
}
function getUser(id: UserId) {
// TypeScript knows this is a UserId, not just any string
}Discriminated Unions
Use discriminant properties for type safety:
type ApiResponse<T> =
| { status: "success"; data: T }
| { status: "error"; error: string };
function handleResponse<T>(response: ApiResponse<T>) {
if (response.status === "success") {
// TypeScript knows response.data exists and is of type T
console.log(response.data);
} else {
// TypeScript knows response.error exists
console.error(response.error);
}
}Function Overloads with Types
function createElement(tag: "input"): HTMLInputElement;
function createElement(tag: "div"): HTMLDivElement;
function createElement(tag: string): HTMLElement {
return document.createElement(tag);
}
const input = createElement("input"); // HTMLInputElement
const div = createElement("div"); // HTMLDivElementUtility Type Combinations
API Response Types
type ApiEndpoint = {
"/users": {
GET: { response: User[] };
POST: { body: Omit<User, "id">; response: User };
};
"/users/:id": {
GET: { params: { id: string }; response: User };
PUT: { params: { id: string }; body: Partial<User>; response: User };
DELETE: { params: { id: string }; response: { success: boolean } };
};
};
type ExtractResponse<
Endpoint extends keyof ApiEndpoint,
Method extends keyof ApiEndpoint[Endpoint],
> = ApiEndpoint[Endpoint][Method] extends { response: infer R } ? R : never;
type GetUsersResponse = ExtractResponse<"/users", "GET">; // User[]
type CreateUserResponse = ExtractResponse<"/users", "POST">; // UserForm Validation Types
type ValidationRule<T> = {
required?: boolean;
minLength?: T extends string ? number : never;
maxLength?: T extends string ? number : never;
pattern?: T extends string ? RegExp : never;
custom?: (value: T) => boolean | string;
};
type ValidationSchema<T> = {
[K in keyof T]: ValidationRule<T[K]>;
};
type UserForm = {
name: string;
email: string;
age: number;
};
const userValidation: ValidationSchema<UserForm> = {
name: { required: true, minLength: 2 },
email: { required: true, pattern: /^[^\s@]+@[^\s@]+\.[^\s@]+$/ },
age: { required: true, custom: (age) => age >= 18 || "Must be 18+" },
};Best Practices
Type Organization
// types/index.ts
export * from './user';
export * from './api';
export * from './validation';
// types/user.ts
export interface User { /* ... */ }
export type UserId = string & { readonly __brand: 'UserId' };
// types/api.ts
export type ApiResponse<T> = /* ... */;
export type ApiEndpoint = /* ... */;Generic Constraints
type ComponentProps<T> = T extends React.ComponentType<infer P> ? P : never;
type ButtonProps = ComponentProps<typeof Button>;Type Assertions vs Type Guards
// Type guard (preferred)
function isUser(obj: any): obj is User {
return obj && typeof obj.id === "number" && typeof obj.name === "string";
}
// Type assertion (use sparingly)
const user = someValue as User; // Can be unsafeConclusion
Advanced TypeScript patterns provide powerful tools for creating type-safe, maintainable code. Understanding utility types, mapped types, and conditional types allows you to express complex type relationships and catch errors at compile time rather than runtime.