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 optional
type PartialUser = Partial<User>;
// { id?: number; name?: string; email?: string; age?: number; isActive?: boolean; }
// Required - makes all properties required
type RequiredUser = Required<User>;
// { id: number; name: string; email: string; age: number; isActive: boolean; }
// Readonly - makes all properties readonly
type ReadonlyUser = Readonly<User>;
// { readonly id: number; readonly name: string; ... }
// Record - creates object type with specific keys and values
type UserRoles = Record<"admin" | "user" | "guest", User>;
// { admin: User; user: User; guest: User; }

Selection and Filtering Utilities

// Pick - select specific properties
type UserSummary = Pick<User, "id" | "name" | "email">;
// { id: number; name: string; email: string; }
// Omit - exclude specific properties
type CreateUserRequest = Omit<User, "id">;
// { name: string; email: string; age?: number; isActive: boolean; }
// Extract - extract types from union that are assignable to another type
type StringOrNumber = string | number | boolean;
type OnlyStringOrNumber = Extract<StringOrNumber, string | number>;
// string | number
// Exclude - exclude types from union
type OnlyBoolean = Exclude<StringOrNumber, string | number>;
// boolean
// NonNullable - exclude null and undefined
type NonNullableString = NonNullable<string | null | undefined>;
// string

Function Utilities

function createUser(name: string, email: string): Promise<User> {
return Promise.resolve({ id: 1, name, email, isActive: true });
}
// Parameters - extract parameter types
type CreateUserParams = Parameters<typeof createUser>;
// [string, string]
// ReturnType - extract return type
type CreateUserReturn = ReturnType<typeof createUser>;
// Promise<User>
// ConstructorParameters - extract constructor parameter types
class UserService {
constructor(private apiUrl: string, private timeout: number) {}
}
type UserServiceParams = ConstructorParameters<typeof UserService>;
// [string, number]
// InstanceType - extract instance type from constructor
type UserServiceInstance = InstanceType<typeof UserService>;
// UserService

Creating Custom Utility Types

Deep Transformation Utilities

// Deep partial - makes all nested properties optional
type 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 required
type 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 readonly
type 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 readonly

Conditional Utility Types

// Make specific properties optional
type Optional<T, K extends keyof T> = Omit<T, K> & Partial<Pick<T, K>>;
// Make specific properties required
type RequiredKeys<T, K extends keyof T> = Required<Pick<T, K>> & Omit<T, K>;
// Nullable version of specific properties
type 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 type
type PickByType<T, U> = {
[K in keyof T as T[K] extends U ? K : never]: T[K];
};
// Omit properties by type
type OmitByType<T, U> = {
[K in keyof T as T[K] extends U ? never : K]: T[K];
};
// Get function property names
type FunctionPropertyNames<T> = {
[K in keyof T]: T[K] extends Function ? K : never;
}[keyof T];
// Get non-function property names
type 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 first
type Merge<T, U> = Omit<T, keyof U> & U;
// Deep merge two types
type 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 unions
type 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 path
type 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 path
type 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 paths
type 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 types
type Compose<F, G> = F extends (arg: infer A) => infer B
? G extends (arg: B) => infer C
? (arg: A) => C
: never
: never;
// Pipe function types
type 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) => string
type Piped = Pipe<[AddOne, ToString, GetLength]>; // (x: number) => number
// Curry function type
type 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) => number

Practical Utility Type Libraries

Form Validation Utilities

// Validation rule types
type 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 schema
type ValidationSchema<T> = {
[K in keyof T]: ValidationRule<T[K]>;
};
// Generate error types
type ValidationErrors<T> = {
[K in keyof T]?: string[];
};
// Field state for forms
type FieldState<T> = {
value: T;
error?: string;
touched: boolean;
dirty: boolean;
};
// Form state
type 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 methods
type HttpMethod = "GET" | "POST" | "PUT" | "DELETE" | "PATCH";
// API endpoint definition
type ApiEndpoint<M extends HttpMethod, P extends string, B = never, R = any> = {
method: M;
path: P;
body: B;
response: R;
};
// Extract route parameters
type 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 name
type ClientMethodName<M extends HttpMethod, P extends string> =
`${Lowercase<M>}${Capitalize<CamelCase<Replace<P, "/", "_">>>}`;
// API client type generation
type 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 API
type 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 generation
type ActionType<T extends string, P = void> = P extends void
? { type: T }
: { type: T; payload: P };
// Generate actions from action map
type Actions<T extends Record<string, any>> = {
[K in keyof T]: ActionType<K & string, T[K]>;
}[keyof T];
// Reducer type
type Reducer<S, A> = (state: S, action: A) => S;
// State slice definition
type StateSlice<T, A extends Record<string, any>> = {
initialState: T;
reducers: {
[K in keyof A]: (state: T, action: ActionType<K & string, A[K]>) => T;
};
};
// Action creators
type 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 usage
interface 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 types
type LazyPick<T, K> = K extends keyof T ? Pick<T, K> : never;
// Memoized type computation
type Memoize<T, K extends PropertyKey, V> = T & { [P in K]: V };
// Efficient union handling
type DistributeOver<T, U> = T extends any ? U<T> : never;
// Example: Efficient property extraction
type 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 recursion
type 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 performance
type 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 framework
type 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 cases
type 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 validation
function 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 composed
type 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 transformations
type 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 objects
type SafeUtility<T> = keyof T extends never ? {} : TransformType<T>;
// Handle never types
type SafeTransform<T> = [T] extends [never] ? never : Transform<T>;
// Handle union types properly
type DistributiveUtility<T> = T extends any ? Transform<T> : never;

4. Optimize for Common Use Cases

// Provide shortcuts for common patterns
type 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.

Share Feedback