Back to Blog
TypeScriptUtility TypesType TransformationsGenerics

TypeScript Utility Types Deep Dive

Master TypeScript's built-in utility types for powerful type transformations and manipulation.

B
Bootspring Team
Engineering
March 20, 2019
7 min read

TypeScript provides powerful utility types for common type transformations. Here's a comprehensive guide to using them effectively.

Partial and Required#

1interface User { 2 id: number; 3 name: string; 4 email: string; 5 age?: number; 6} 7 8// Make all properties optional 9type PartialUser = Partial<User>; 10// { id?: number; name?: string; email?: string; age?: number; } 11 12// Make all properties required 13type RequiredUser = Required<User>; 14// { id: number; name: string; email: string; age: number; } 15 16// Practical use: update functions 17function updateUser(id: number, updates: Partial<User>): User { 18 const user = getUser(id); 19 return { ...user, ...updates }; 20} 21 22updateUser(1, { name: 'John' }); // Only update name

Readonly and Mutable#

1interface Config { 2 apiUrl: string; 3 timeout: number; 4 retries: number; 5} 6 7// Make all properties readonly 8type ReadonlyConfig = Readonly<Config>; 9// { readonly apiUrl: string; readonly timeout: number; readonly retries: number; } 10 11const config: ReadonlyConfig = { 12 apiUrl: 'https://api.example.com', 13 timeout: 5000, 14 retries: 3, 15}; 16 17// config.timeout = 10000; // Error: Cannot assign to 'timeout' 18 19// Create mutable version (custom utility) 20type Mutable<T> = { 21 -readonly [P in keyof T]: T[P]; 22}; 23 24type MutableConfig = Mutable<ReadonlyConfig>;

Pick and Omit#

1interface Article { 2 id: number; 3 title: string; 4 content: string; 5 author: string; 6 createdAt: Date; 7 updatedAt: Date; 8} 9 10// Pick specific properties 11type ArticlePreview = Pick<Article, 'id' | 'title' | 'author'>; 12// { id: number; title: string; author: string; } 13 14// Omit specific properties 15type ArticleInput = Omit<Article, 'id' | 'createdAt' | 'updatedAt'>; 16// { title: string; content: string; author: string; } 17 18// Combine for complex types 19type EditableArticle = Omit<Article, 'id' | 'createdAt'> & { id: number };

Record#

1// Create object type with specific keys 2type Status = 'pending' | 'approved' | 'rejected'; 3type StatusMessages = Record<Status, string>; 4 5const messages: StatusMessages = { 6 pending: 'Awaiting review', 7 approved: 'Application approved', 8 rejected: 'Application rejected', 9}; 10 11// Dynamic keys 12type UserRoles = Record<string, string[]>; 13 14const roles: UserRoles = { 15 admin: ['read', 'write', 'delete'], 16 user: ['read'], 17 guest: [], 18}; 19 20// Numeric keys 21type IndexedItems = Record<number, { name: string }>;

Extract and Exclude#

1type AllTypes = string | number | boolean | null | undefined; 2 3// Extract types that extend a condition 4type Primitives = Extract<AllTypes, string | number | boolean>; 5// string | number | boolean 6 7// Exclude types that extend a condition 8type NonNullTypes = Exclude<AllTypes, null | undefined>; 9// string | number | boolean 10 11// Practical use: event types 12type MouseEvents = 'click' | 'dblclick' | 'mousedown' | 'mouseup'; 13type KeyboardEvents = 'keydown' | 'keyup' | 'keypress'; 14type AllEvents = MouseEvents | KeyboardEvents; 15 16type OnlyMouseEvents = Extract<AllEvents, MouseEvents>; 17type WithoutMouseEvents = Exclude<AllEvents, MouseEvents>;

NonNullable#

1type MaybeString = string | null | undefined; 2type DefinitelyString = NonNullable<MaybeString>; 3// string 4 5// Practical use 6function processValue<T>(value: T): NonNullable<T> { 7 if (value === null || value === undefined) { 8 throw new Error('Value cannot be null or undefined'); 9 } 10 return value as NonNullable<T>; 11} 12 13const result = processValue<string | null>('hello'); 14// result is string, not string | null

ReturnType and Parameters#

1function createUser(name: string, age: number): { id: number; name: string; age: number } { 2 return { id: Date.now(), name, age }; 3} 4 5// Get return type 6type User = ReturnType<typeof createUser>; 7// { id: number; name: string; age: number } 8 9// Get parameter types 10type CreateUserParams = Parameters<typeof createUser>; 11// [string, number] 12 13// Useful for wrapping functions 14function loggedCreateUser(...args: Parameters<typeof createUser>): ReturnType<typeof createUser> { 15 console.log('Creating user with:', args); 16 return createUser(...args); 17}

InstanceType and ConstructorParameters#

1class ApiClient { 2 constructor(baseUrl: string, timeout: number) { 3 // ... 4 } 5} 6 7// Get instance type 8type Client = InstanceType<typeof ApiClient>; 9// ApiClient 10 11// Get constructor parameters 12type ClientParams = ConstructorParameters<typeof ApiClient>; 13// [string, number] 14 15// Factory function 16function createClient(...args: ConstructorParameters<typeof ApiClient>): InstanceType<typeof ApiClient> { 17 return new ApiClient(...args); 18}

ThisParameterType and OmitThisParameter#

1function greet(this: { name: string }, greeting: string): string { 2 return `${greeting}, ${this.name}!`; 3} 4 5// Get 'this' type 6type GreetThis = ThisParameterType<typeof greet>; 7// { name: string } 8 9// Remove 'this' parameter 10type GreetWithoutThis = OmitThisParameter<typeof greet>; 11// (greeting: string) => string 12 13// Bind function 14const boundGreet: GreetWithoutThis = greet.bind({ name: 'World' }); 15boundGreet('Hello'); // 'Hello, World!'

Awaited#

1type PromiseString = Promise<string>; 2type ResolvedString = Awaited<PromiseString>; 3// string 4 5// Works with nested promises 6type NestedPromise = Promise<Promise<Promise<number>>>; 7type ResolvedNumber = Awaited<NestedPromise>; 8// number 9 10// Useful for async function return types 11async function fetchData(): Promise<{ data: string }> { 12 return { data: 'result' }; 13} 14 15type FetchResult = Awaited<ReturnType<typeof fetchData>>; 16// { data: string }

Uppercase, Lowercase, Capitalize, Uncapitalize#

1type Greeting = 'hello world'; 2 3type Upper = Uppercase<Greeting>; // 'HELLO WORLD' 4type Lower = Lowercase<'HELLO WORLD'>; // 'hello world' 5type Cap = Capitalize<Greeting>; // 'Hello world' 6type Uncap = Uncapitalize<'Hello'>; // 'hello' 7 8// Practical use: event handlers 9type EventName = 'click' | 'focus' | 'blur'; 10type HandlerName = `on${Capitalize<EventName>}`; 11// 'onClick' | 'onFocus' | 'onBlur' 12 13type Handlers = { 14 [K in EventName as `on${Capitalize<K>}`]: (event: Event) => void; 15}; 16// { onClick: ..., onFocus: ..., onBlur: ... }

Combining Utility Types#

1interface Entity { 2 id: number; 3 createdAt: Date; 4 updatedAt: Date; 5} 6 7interface User extends Entity { 8 name: string; 9 email: string; 10 role: 'admin' | 'user'; 11} 12 13// Create input type (without auto-generated fields) 14type CreateUserInput = Omit<User, keyof Entity>; 15// { name: string; email: string; role: 'admin' | 'user' } 16 17// Create update type (all fields optional except id) 18type UpdateUserInput = Partial<Omit<User, 'id'>> & Pick<User, 'id'>; 19// { id: number; name?: string; email?: string; ... } 20 21// Readonly entity 22type ReadonlyUser = Readonly<User>; 23 24// Pick with partial 25type PartialPick<T, K extends keyof T> = Partial<Pick<T, K>>; 26type OptionalUserInfo = PartialPick<User, 'name' | 'email'>; 27// { name?: string; email?: string }

Custom Utility Types#

1// Deep Partial 2type DeepPartial<T> = { 3 [P in keyof T]?: T[P] extends object ? DeepPartial<T[P]> : T[P]; 4}; 5 6// Deep Readonly 7type DeepReadonly<T> = { 8 readonly [P in keyof T]: T[P] extends object ? DeepReadonly<T[P]> : T[P]; 9}; 10 11// Nullable 12type Nullable<T> = T | null; 13 14// Optional keys 15type OptionalKeys<T, K extends keyof T> = Omit<T, K> & Partial<Pick<T, K>>; 16 17// Required keys 18type RequiredKeys<T, K extends keyof T> = Omit<T, K> & Required<Pick<T, K>>; 19 20// Pick by type 21type PickByType<T, U> = { 22 [P in keyof T as T[P] extends U ? P : never]: T[P]; 23}; 24 25// Usage 26interface Mixed { 27 id: number; 28 name: string; 29 active: boolean; 30 count: number; 31} 32 33type StringProps = PickByType<Mixed, string>; 34// { name: string } 35 36type NumberProps = PickByType<Mixed, number>; 37// { id: number; count: number }

Best Practices#

Common Patterns: ✓ Partial for update functions ✓ Pick/Omit for API responses ✓ Record for dictionaries ✓ ReturnType for function wrappers Type Safety: ✓ Use Readonly for constants ✓ Use NonNullable after null checks ✓ Combine utilities for complex types ✓ Create custom utilities for reuse Avoid: ✗ Over-nesting utility types ✗ Complex type gymnastics ✗ Ignoring readability ✗ Reinventing built-in types Performance: ✓ Keep type complexity manageable ✓ Use type aliases for readability ✓ Document complex transformations ✓ Test types with assertions

Conclusion#

TypeScript's utility types provide powerful, composable tools for type transformations. Use Partial and Required for optionality, Pick and Omit for property selection, Record for dictionaries, and Extract/Exclude for union manipulation. Combine them to create precise types for your domain, and build custom utilities for patterns specific to your codebase. These tools enable type-safe APIs while keeping your code DRY.

Share this article

Help spread the word about Bootspring