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 namePick 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>>; // UserTemplate 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 setCustom 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.