Utility Types & Type Composition
TypeScript’s utility types are the building blocks of advanced type systems. Understanding how to create, compose, and optimize utility types is essential for building maintainable, scalable TypeScript applications.
Built-in Utility Types Deep Dive
Core Transformation Utilities
interface User { id: number; name: string; email: string; age?: number; isActive: boolean;}
// Partial - makes all properties optionaltype PartialUser = Partial<User>;// { id?: number; name?: string; email?: string; age?: number; isActive?: boolean; }
// Required - makes all properties requiredtype RequiredUser = Required<User>;// { id: number; name: string; email: string; age: number; isActive: boolean; }
// Readonly - makes all properties readonlytype ReadonlyUser = Readonly<User>;// { readonly id: number; readonly name: string; ... }
// Record - creates object type with specific keys and valuestype UserRoles = Record<"admin" | "user" | "guest", User>;// { admin: User; user: User; guest: User; }Selection and Filtering Utilities
// Pick - select specific propertiestype UserSummary = Pick<User, "id" | "name" | "email">;// { id: number; name: string; email: string; }
// Omit - exclude specific propertiestype CreateUserRequest = Omit<User, "id">;// { name: string; email: string; age?: number; isActive: boolean; }
// Extract - extract types from union that are assignable to another typetype StringOrNumber = string | number | boolean;type OnlyStringOrNumber = Extract<StringOrNumber, string | number>;// string | number
// Exclude - exclude types from uniontype OnlyBoolean = Exclude<StringOrNumber, string | number>;// boolean
// NonNullable - exclude null and undefinedtype NonNullableString = NonNullable<string | null | undefined>;// stringFunction Utilities
function createUser(name: string, email: string): Promise<User> { return Promise.resolve({ id: 1, name, email, isActive: true });}
// Parameters - extract parameter typestype CreateUserParams = Parameters<typeof createUser>;// [string, string]
// ReturnType - extract return typetype CreateUserReturn = ReturnType<typeof createUser>;// Promise<User>
// ConstructorParameters - extract constructor parameter typesclass UserService { constructor(private apiUrl: string, private timeout: number) {}}
type UserServiceParams = ConstructorParameters<typeof UserService>;// [string, number]
// InstanceType - extract instance type from constructortype UserServiceInstance = InstanceType<typeof UserService>;// UserServiceCreating Custom Utility Types
Deep Transformation Utilities
// Deep partial - makes all nested properties optionaltype DeepPartial<T> = { [P in keyof T]?: T[P] extends (infer U)[] ? DeepPartial<U>[] : T[P] extends object ? DeepPartial<T[P]> : T[P];};
// Deep required - makes all nested properties requiredtype DeepRequired<T> = { [P in keyof T]-?: T[P] extends (infer U)[] ? DeepRequired<U>[] : T[P] extends object ? DeepRequired<T[P]> : T[P];};
// Deep readonly - makes all nested properties readonlytype DeepReadonly<T> = { readonly [P in keyof T]: T[P] extends (infer U)[] ? readonly DeepReadonly<U>[] : T[P] extends object ? DeepReadonly<T[P]> : T[P];};
interface NestedConfig { database: { host: string; port: number; credentials: { username: string; password: string; }; }; features: { enabled: boolean; options: string[]; };}
type PartialConfig = DeepPartial<NestedConfig>;// All properties at all levels are optional
type ReadonlyConfig = DeepReadonly<NestedConfig>;// All properties at all levels are readonlyConditional Utility Types
// Make specific properties optionaltype Optional<T, K extends keyof T> = Omit<T, K> & Partial<Pick<T, K>>;
// Make specific properties requiredtype RequiredKeys<T, K extends keyof T> = Required<Pick<T, K>> & Omit<T, K>;
// Nullable version of specific propertiestype Nullable<T, K extends keyof T> = { [P in keyof T]: P extends K ? T[P] | null : T[P];};
type UserWithOptionalAge = Optional<User, "age">;// { id: number; name: string; email: string; isActive: boolean; age?: number; }
type UserWithRequiredAge = RequiredKeys<Partial<User>, "age">;// { age: number; id?: number; name?: string; email?: string; isActive?: boolean; }
type UserWithNullableEmail = Nullable<User, "email">;// { id: number; name: string; email: string | null; age?: number; isActive: boolean; }Type Filtering Utilities
// Pick properties by typetype PickByType<T, U> = { [K in keyof T as T[K] extends U ? K : never]: T[K];};
// Omit properties by typetype OmitByType<T, U> = { [K in keyof T as T[K] extends U ? never : K]: T[K];};
// Get function property namestype FunctionPropertyNames<T> = { [K in keyof T]: T[K] extends Function ? K : never;}[keyof T];
// Get non-function property namestype NonFunctionPropertyNames<T> = { [K in keyof T]: T[K] extends Function ? never : K;}[keyof T];
interface UserService { id: number; name: string; isActive: boolean; save(): Promise<void>; delete(): Promise<void>; validate(data: any): boolean;}
type UserData = PickByType<UserService, string | number | boolean>;// { id: number; name: string; isActive: boolean; }
type UserMethods = PickByType<UserService, Function>;// { save: () => Promise<void>; delete: () => Promise<void>; validate: (data: any) => boolean; }
type FunctionNames = FunctionPropertyNames<UserService>;// "save" | "delete" | "validate"Advanced Type Composition
Intersection and Union Utilities
// Merge two types, with the second overriding the firsttype Merge<T, U> = Omit<T, keyof U> & U;
// Deep merge two typestype DeepMerge<T, U> = { [K in keyof T | keyof U]: K extends keyof U ? K extends keyof T ? T[K] extends object ? U[K] extends object ? DeepMerge<T[K], U[K]> : U[K] : U[K] : U[K] : K extends keyof T ? T[K] : never;};
interface BaseUser { id: number; name: string; email: string;}
interface AdminUser { email: string; // Different type constraint permissions: string[]; lastLogin: Date;}
type MergedUser = Merge<BaseUser, AdminUser>;// { id: number; name: string; email: string; permissions: string[]; lastLogin: Date; }
// Create discriminated unionstype CreateDiscriminatedUnion<T, K extends keyof T> = { [P in keyof T]: { type: P } & T[P];}[keyof T];
interface ShapeTypes { circle: { radius: number }; rectangle: { width: number; height: number }; triangle: { base: number; height: number };}
type Shape = CreateDiscriminatedUnion<ShapeTypes, "type">;// { type: "circle"; radius: number } |// { type: "rectangle"; width: number; height: number } |// { type: "triangle"; base: number; height: number }Path-Based Type Access
// Get nested property type by pathtype GetByPath<T, P extends string> = P extends `${infer Key}.${infer Rest}` ? Key extends keyof T ? GetByPath<T[Key], Rest> : never : P extends keyof T ? T[P] : never;
// Set nested property type by pathtype SetByPath<T, P extends string, V> = P extends `${infer Key}.${infer Rest}` ? Key extends keyof T ? { [K in keyof T]: K extends Key ? SetByPath<T[K], Rest, V> : T[K] } : never : P extends keyof T ? { [K in keyof T]: K extends P ? V : T[K] } : never;
// Generate all possible pathstype Paths<T, Prefix extends string = ""> = { [K in keyof T]: T[K] extends object ? K extends string ? `${Prefix}${K}` | Paths<T[K], `${Prefix}${K}.`> : never : K extends string ? `${Prefix}${K}` : never;}[keyof T];
interface NestedUser { profile: { personal: { name: string; age: number; }; settings: { theme: "light" | "dark"; notifications: boolean; }; };}
type UserName = GetByPath<NestedUser, "profile.personal.name">;// string
type UserPaths = Paths<NestedUser>;// "profile" | "profile.personal" | "profile.personal.name" | "profile.personal.age" |// "profile.settings" | "profile.settings.theme" | "profile.settings.notifications"Functional Type Composition
// Compose function typestype Compose<F, G> = F extends (arg: infer A) => infer B ? G extends (arg: B) => infer C ? (arg: A) => C : never : never;
// Pipe function typestype Pipe<T extends readonly any[]> = T extends readonly [ (arg: infer A) => infer B, ...infer Rest] ? Rest extends readonly [(arg: B) => any, ...any[]] ? Pipe<Rest> extends (arg: B) => infer C ? (arg: A) => C : never : (arg: A) => B : never;
type AddOne = (x: number) => number;type ToString = (x: number) => string;type GetLength = (x: string) => number;
type Composed = Compose<AddOne, ToString>; // (x: number) => stringtype Piped = Pipe<[AddOne, ToString, GetLength]>; // (x: number) => number
// Curry function typetype Curry<T> = T extends (arg: infer A, ...rest: infer R) => infer Return ? R extends [] ? (arg: A) => Return : (arg: A) => Curry<(...args: R) => Return> : never;
function add(a: number, b: number, c: number): number { return a + b + c;}
type CurriedAdd = Curry<typeof add>;// (arg: number) => (arg: number) => (arg: number) => numberPractical Utility Type Libraries
Form Validation Utilities
// Validation rule typestype ValidationRule<T> = { required?: boolean; min?: T extends string | any[] ? number : T extends number ? T : never; max?: T extends string | any[] ? number : T extends number ? T : never; pattern?: T extends string ? RegExp : never; custom?: (value: T) => boolean | string;};
// Generate validation schematype ValidationSchema<T> = { [K in keyof T]: ValidationRule<T[K]>;};
// Generate error typestype ValidationErrors<T> = { [K in keyof T]?: string[];};
// Field state for formstype FieldState<T> = { value: T; error?: string; touched: boolean; dirty: boolean;};
// Form statetype FormState<T> = { [K in keyof T]: FieldState<T[K]>;} & { isValid: boolean; isSubmitting: boolean; errors: ValidationErrors<T>;};
interface LoginForm { username: string; password: string; rememberMe: boolean;}
type LoginSchema = ValidationSchema<LoginForm>;type LoginState = FormState<LoginForm>;type LoginErrors = ValidationErrors<LoginForm>;API Client Utilities
// HTTP methodstype HttpMethod = "GET" | "POST" | "PUT" | "DELETE" | "PATCH";
// API endpoint definitiontype ApiEndpoint<M extends HttpMethod, P extends string, B = never, R = any> = { method: M; path: P; body: B; response: R;};
// Extract route parameterstype ExtractParams<T extends string> = T extends `${string}:${infer Param}/${infer Rest}` ? { [K in Param]: string } & ExtractParams<`/${Rest}`> : T extends `${string}:${infer Param}` ? { [K in Param]: string } : {};
// Generate client method nametype ClientMethodName<M extends HttpMethod, P extends string> = `${Lowercase<M>}${Capitalize<CamelCase<Replace<P, "/", "_">>>}`;
// API client type generationtype ApiClient<T extends Record<string, ApiEndpoint<any, any, any, any>>> = { [K in keyof T as T[K] extends ApiEndpoint<infer M, infer P, any, any> ? ClientMethodName<M, P> : never ]: T[K] extends ApiEndpoint<any, infer P, infer B, infer R> ? ( ...args: ExtractParams<P> extends Record<string, never> ? B extends never ? [] : [body: B] : B extends never ? [params: ExtractParams<P>] : [params: ExtractParams<P>, body: B] ) => Promise<R> : never;};
// Define APItype UserApi = { getUsers: ApiEndpoint<"GET", "/users", never, User[]>; createUser: ApiEndpoint<"POST", "/users", CreateUserRequest, User>; getUser: ApiEndpoint<"GET", "/users/:id", never, User>; updateUser: ApiEndpoint<"PUT", "/users/:id", Partial<User>, User>; deleteUser: ApiEndpoint<"DELETE", "/users/:id", never, void>;};
type UserClient = ApiClient<UserApi>;// {// getUsers: () => Promise<User[]>;// postUsers: (body: CreateUserRequest) => Promise<User>;// getUsersId: (params: { id: string }) => Promise<User>;// putUsersId: (params: { id: string }, body: Partial<User>) => Promise<User>;// deleteUsersId: (params: { id: string }) => Promise<void>;// }State Management Utilities
// Action type generationtype ActionType<T extends string, P = void> = P extends void ? { type: T } : { type: T; payload: P };
// Generate actions from action maptype Actions<T extends Record<string, any>> = { [K in keyof T]: ActionType<K & string, T[K]>;}[keyof T];
// Reducer typetype Reducer<S, A> = (state: S, action: A) => S;
// State slice definitiontype StateSlice<T, A extends Record<string, any>> = { initialState: T; reducers: { [K in keyof A]: (state: T, action: ActionType<K & string, A[K]>) => T; };};
// Action creatorstype ActionCreators<T extends Record<string, any>> = { [K in keyof T]: T[K] extends void ? () => ActionType<K & string, T[K]> : (payload: T[K]) => ActionType<K & string, T[K]>;};
// Example usageinterface UserState { users: User[]; loading: boolean; error: string | null;}
type UserActions = { setLoading: boolean; setError: string | null; setUsers: User[]; addUser: User; removeUser: string; // user ID};
type UserActionTypes = Actions<UserActions>;type UserReducer = Reducer<UserState, UserActionTypes>;type UserActionCreators = ActionCreators<UserActions>;Performance Optimization
Lazy Type Evaluation
// Lazy conditional typestype LazyPick<T, K> = K extends keyof T ? Pick<T, K> : never;
// Memoized type computationtype Memoize<T, K extends PropertyKey, V> = T & { [P in K]: V };
// Efficient union handlingtype DistributeOver<T, U> = T extends any ? U<T> : never;
// Example: Efficient property extractiontype EfficientStringProps<T> = DistributeOver< T, <U>() => U extends Record<PropertyKey, any> ? { [K in keyof U as U[K] extends string ? K : never]: U[K] } : never>;Type Complexity Management
// Depth-limited recursiontype SafeDeepPartial<T, Depth extends number = 5> = Depth extends 0 ? T : { [P in keyof T]?: T[P] extends object ? SafeDeepPartial<T[P], Prev<Depth>> : T[P]; };
type Prev<T extends number> = T extends 5 ? 4 : T extends 4 ? 3 : T extends 3 ? 2 : T extends 2 ? 1 : T extends 1 ? 0 : never;
// Iterative approach for better performancetype IterativeTransform<T, U = {}> = keyof T extends never ? U : T extends { [K in keyof T]: infer V } ? IterativeTransform<Omit<T, keyof T>, U & Record<keyof T, V>> : never;Testing Utility Types
// Type testing frameworktype Expect<T extends true> = T;type Equal<X, Y> = (<T>() => T extends X ? 1 : 2) extends <T>() => T extends Y ? 1 : 2 ? true : false;
type NotEqual<X, Y> = Equal<X, Y> extends true ? false : true;type IsAny<T> = 0 extends 1 & T ? true : false;type IsNever<T> = [T] extends [never] ? true : false;
// Test casestype TestOptional = Expect<Equal< Optional<{ a: string; b: number }, "b">, { a: string; b?: number }>>;
type TestPickByType = Expect<Equal< PickByType<{ a: string; b: number; c: boolean }, string>, { a: string }>>;
type TestDeepPartial = Expect<Equal< DeepPartial<{ a: { b: string } }>, { a?: { b?: string } }>>;
// Runtime validationfunction testUtilityTypes() { // These should compile without errors const optional: Optional<User, "age"> = { id: 1, name: "John", email: "john@example.com", isActive: true }; const stringProps: PickByType<User, string> = { name: "John", email: "john@example.com" };
console.log("Utility type tests passed!");}Best Practices
1. Create Composable Utilities
// ✅ Small, focused utilities that can be composedtype MakeOptional<T, K extends keyof T> = Omit<T, K> & Partial<Pick<T, K>>;type MakeRequired<T, K extends keyof T> = Required<Pick<T, K>> & Omit<T, K>;type MakeNullable<T, K extends keyof T> = Omit<T, K> & { [P in K]: T[P] | null };
// Compose them for complex transformationstype FlexibleUser = MakeOptional<MakeNullable<User, "email">, "age">;2. Provide Clear Documentation
/** * Creates a type where specific keys are optional while others remain required. * * @template T - The source object type * @template K - Keys to make optional (must be keys of T) * * @example * type UserWithOptionalEmail = Optional<User, "email">; * // Result: { id: number; name: string; email?: string; isActive: boolean; } */type Optional<T, K extends keyof T> = Omit<T, K> & Partial<Pick<T, K>>;3. Handle Edge Cases
// Handle empty objectstype SafeUtility<T> = keyof T extends never ? {} : TransformType<T>;
// Handle never typestype SafeTransform<T> = [T] extends [never] ? never : Transform<T>;
// Handle union types properlytype DistributiveUtility<T> = T extends any ? Transform<T> : never;4. Optimize for Common Use Cases
// Provide shortcuts for common patternstype CreateRequest<T> = Omit<T, "id" | "createdAt" | "updatedAt">;type UpdateRequest<T> = Partial<Omit<T, "id" | "createdAt" | "updatedAt">>;type ApiResponse<T> = { data: T; success: boolean; message?: string };
type CreateUserRequest = CreateRequest<User>;type UpdateUserRequest = UpdateRequest<User>;type UserResponse = ApiResponse<User>;Conclusion
Utility types and type composition are fundamental to building sophisticated TypeScript applications. Key takeaways:
- Build composable utilities that can be combined for complex transformations
- Use conditional types for flexible, adaptive type behavior
- Create domain-specific utilities for common patterns in your application
- Optimize for performance with lazy evaluation and depth limits
- Test your types to ensure they work as expected
- Document complex utilities for better maintainability
Mastering these patterns enables you to create type systems that are both powerful and maintainable, providing excellent developer experience while catching errors at compile time.
Next Steps
In the final part of this series, we’ll explore Design Patterns with TypeScript, focusing on implementing classic design patterns with full type safety and modern TypeScript features.