Back to Blog
TypeScriptTypesUtility TypesAdvanced

TypeScript Utility Types Deep Dive

Master TypeScript utility types. From Partial and Pick to custom type manipulation patterns.

B
Bootspring Team
Engineering
August 28, 2022
6 min read

TypeScript's utility types enable powerful type transformations. Here's how to use and create them effectively.

Built-in Utility Types#

Partial and Required#

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

Pick and Omit#

1interface User { 2 id: string; 3 name: string; 4 email: string; 5 password: string; 6 createdAt: Date; 7} 8 9// Select specific properties 10type PublicUser = Pick<User, 'id' | 'name' | 'email'>; 11// { id: string; name: string; email: string; } 12 13// Exclude specific properties 14type UserWithoutPassword = Omit<User, 'password'>; 15// { id: string; name: string; email: string; createdAt: Date; } 16 17// API response type 18type UserResponse = Omit<User, 'password' | 'createdAt'>;

Record#

1// Create object type with specific keys and value type 2type UserRoles = 'admin' | 'user' | 'guest'; 3type RolePermissions = Record<UserRoles, string[]>; 4 5const permissions: RolePermissions = { 6 admin: ['read', 'write', 'delete'], 7 user: ['read', 'write'], 8 guest: ['read'], 9}; 10 11// Dynamic keys 12type Cache<T> = Record<string, T>; 13 14const userCache: Cache<User> = { 15 'user-123': { id: '123', name: 'John', /* ... */ }, 16 'user-456': { id: '456', name: 'Jane', /* ... */ }, 17};

Readonly and Mutable#

1interface Config { 2 apiUrl: string; 3 timeout: number; 4} 5 6// All properties readonly 7type ReadonlyConfig = Readonly<Config>; 8 9const config: ReadonlyConfig = { 10 apiUrl: 'https://api.example.com', 11 timeout: 5000, 12}; 13 14// config.apiUrl = 'new-url'; // Error! 15 16// Deep readonly 17type DeepReadonly<T> = { 18 readonly [P in keyof T]: T[P] extends object 19 ? DeepReadonly<T[P]> 20 : T[P]; 21}; 22 23// Make mutable again 24type Mutable<T> = { 25 -readonly [P in keyof T]: T[P]; 26};

ReturnType and Parameters#

1function createUser(name: string, email: string): User { 2 return { id: crypto.randomUUID(), name, email, createdAt: new Date() }; 3} 4 5// Get return type of function 6type CreateUserReturn = ReturnType<typeof createUser>; 7// User 8 9// Get parameter types 10type CreateUserParams = Parameters<typeof createUser>; 11// [string, string] 12 13// Useful for wrapper functions 14function loggedCreateUser( 15 ...args: Parameters<typeof createUser> 16): ReturnType<typeof createUser> { 17 console.log('Creating user:', args); 18 return createUser(...args); 19}

Advanced Patterns#

Conditional Types#

1// Type that changes based on condition 2type IsString<T> = T extends string ? true : false; 3 4type A = IsString<string>; // true 5type B = IsString<number>; // false 6 7// Extract array element type 8type ArrayElement<T> = T extends (infer U)[] ? U : never; 9 10type StringElement = ArrayElement<string[]>; // string 11type NumberElement = ArrayElement<number[]>; // number 12 13// Unwrap Promise 14type Awaited<T> = T extends Promise<infer U> ? U : T; 15 16type ResolvedUser = Awaited<Promise<User>>; // User

Template Literal Types#

1// Create string patterns 2type EventName = `on${Capitalize<string>}`; 3// 'onClick', 'onChange', etc. 4 5type HttpMethod = 'get' | 'post' | 'put' | 'delete'; 6type ApiPath = '/users' | '/posts' | '/comments'; 7 8type ApiEndpoint = `${Uppercase<HttpMethod>} ${ApiPath}`; 9// 'GET /users' | 'GET /posts' | ... | 'DELETE /comments' 10 11// Extract parts from string 12type ExtractRouteParams<T extends string> = 13 T extends `${string}:${infer Param}/${infer Rest}` 14 ? Param | ExtractRouteParams<`/${Rest}`> 15 : T extends `${string}:${infer Param}` 16 ? Param 17 : never; 18 19type Params = ExtractRouteParams<'/users/:userId/posts/:postId'>; 20// 'userId' | 'postId'

Mapped Types#

1// Transform all properties 2type Getters<T> = { 3 [K in keyof T as `get${Capitalize<string & K>}`]: () => T[K]; 4}; 5 6interface Person { 7 name: string; 8 age: number; 9} 10 11type PersonGetters = Getters<Person>; 12// { getName: () => string; getAge: () => number; } 13 14// Filter properties by type 15type OnlyStrings<T> = { 16 [K in keyof T as T[K] extends string ? K : never]: T[K]; 17}; 18 19type StringProps = OnlyStrings<User>; 20// { id: string; name: string; email: string; } 21 22// Make all properties nullable 23type Nullable<T> = { 24 [K in keyof T]: T[K] | null; 25};

Discriminated Unions#

1// Type-safe state management 2type LoadingState = { status: 'loading' }; 3type SuccessState<T> = { status: 'success'; data: T }; 4type ErrorState = { status: 'error'; error: string }; 5 6type AsyncState<T> = LoadingState | SuccessState<T> | ErrorState; 7 8function handleState(state: AsyncState<User>): void { 9 switch (state.status) { 10 case 'loading': 11 console.log('Loading...'); 12 break; 13 case 'success': 14 console.log('User:', state.data.name); // TypeScript knows data exists 15 break; 16 case 'error': 17 console.log('Error:', state.error); // TypeScript knows error exists 18 break; 19 } 20}

Builder Pattern Types#

1// Type-safe builder 2type Builder<T, Built extends Partial<T> = {}> = { 3 set<K extends keyof T>( 4 key: K, 5 value: T[K] 6 ): Builder<T, Built & Pick<T, K>>; 7 build(): Built extends T ? T : never; 8}; 9 10interface Config { 11 host: string; 12 port: number; 13 ssl: boolean; 14} 15 16function createBuilder<T>(): Builder<T, {}> { 17 const values: Partial<T> = {}; 18 19 return { 20 set(key, value) { 21 values[key] = value; 22 return this as any; 23 }, 24 build() { 25 return values as any; 26 }, 27 }; 28} 29 30const config = createBuilder<Config>() 31 .set('host', 'localhost') 32 .set('port', 3000) 33 .set('ssl', true) 34 .build(); 35// TypeScript ensures all required fields are set

Custom Utility Types#

1// Make specific keys optional 2type PartialBy<T, K extends keyof T> = Omit<T, K> & Partial<Pick<T, K>>; 3 4type UserWithOptionalEmail = PartialBy<User, 'email'>; 5 6// Make specific keys required 7type RequiredBy<T, K extends keyof T> = T & Required<Pick<T, K>>; 8 9// Get keys by value type 10type KeysOfType<T, V> = { 11 [K in keyof T]: T[K] extends V ? K : never; 12}[keyof T]; 13 14type StringKeys = KeysOfType<User, string>; // 'id' | 'name' | 'email' 15 16// Deep partial 17type DeepPartial<T> = { 18 [P in keyof T]?: T[P] extends object ? DeepPartial<T[P]> : T[P]; 19}; 20 21// Merge two types 22type Merge<A, B> = Omit<A, keyof B> & B; 23 24type Base = { a: string; b: number }; 25type Override = { b: string; c: boolean }; 26type Merged = Merge<Base, Override>; 27// { a: string; b: string; c: boolean }

Best Practices#

Usage: ✓ Use built-in types when possible ✓ Create reusable custom types ✓ Document complex type transformations ✓ Keep types readable Performance: ✓ Avoid deeply nested conditionals ✓ Use type aliases for readability ✓ Consider type inference limits ✓ Test with edge cases Organization: ✓ Group related utility types ✓ Export from dedicated files ✓ Version breaking changes ✓ Add JSDoc comments

Conclusion#

TypeScript's utility types enable powerful type transformations. Master the built-in types first, then combine them for complex scenarios. Well-designed types catch errors early and improve code documentation.

Share this article

Help spread the word about Bootspring