TypeScript's utility types are powerful tools that help you manipulate and transform types in your codebase. Understanding these advanced features can significantly improve your code's type safety and maintainability. Let's dive deep into the most useful utility types and their practical applications.
Built-in Utility Types
Partial and Required
These utility types modify the optionality of object properties:
interface User {
id: number;
name: string;
email: string;
age: number;
}
// All properties become optional
type PartialUser = Partial<User>;
// All properties become required
type RequiredUser = Required<User>;
Pick and Omit
Select specific properties or remove them from a type:
// Only keep these properties
type UserBasics = Pick<User, 'id' | 'name'>;
// Remove these properties
type UserWithoutPersonal = Omit<User, 'email' | 'age'>;
Advanced Type Manipulation
Record Type
Create an object type with specified keys and value types:
type UserRoles = 'admin' | 'user' | 'guest';
type Permissions = 'read' | 'write' | 'delete';
type RolePermissions = Record<UserRoles, Permissions[]>;
const permissions: RolePermissions = {
admin: ['read', 'write', 'delete'],
user: ['read', 'write'],
guest: ['read']
};
Conditional Types
Create types that depend on type conditions:
type IsString<T> = T extends string ? true : false;
// Usage examples
type Result1 = IsString<'hello'>; // true
type Result2 = IsString<42>; // false
// More practical example
type ArrayOrSingle<T> = T extends any[] ? T : T[];
function processItems<T>(items: ArrayOrSingle<T>): T[] {
return Array.isArray(items) ? items : [items];
}
Template Literal Types
Create complex string literal types:
type HttpMethod = 'GET' | 'POST' | 'PUT' | 'DELETE';
type Path = '/users' | '/posts' | '/comments';
type Endpoint = `${HttpMethod} ${Path}`;
function fetchAPI(endpoint: Endpoint) {
// Type-safe API calls
}
fetchAPI('GET /users'); // Valid
fetchAPI('PATCH /posts'); // Error!
Mapped Types
Create new types by transforming existing ones:
type Readonly<T> = {
readonly [P in keyof T]: T[P];
};
type Optional<T> = {
[P in keyof T]?: T[P];
};
// Custom mapped type example
type Nullable<T> = {
[P in keyof T]: T[P] | null;
};
Advanced Mapped Type Modifiers
Combine modifiers for complex type transformations:
type ReadonlyNullable<T> = {
readonly [P in keyof T]: T[P] | null;
};
// Remove optional modifier
type Required<T> = {
[P in keyof T]-?: T[P];
};
Practical Applications
Form Handling
Create type-safe form handling utilities:
interface FormFields {
username: string;
email: string;
password: string;
}
type FormErrors = Partial<Record<keyof FormFields, string>>;
class FormHandler {
private values: FormFields = {
username: '',
email: '',
password: ''
};
private errors: FormErrors = {};
setField<K extends keyof FormFields>(
field: K,
value: FormFields[K]
) {
this.values[field] = value;
}
setError<K extends keyof FormFields>(
field: K,
error: string | undefined
) {
if (error) {
this.errors[field] = error;
} else {
delete this.errors[field];
}
}
}
API Response Types
Handle different API response states:
type ApiResponse<T> = {
data: T | null;
error: string | null;
loading: boolean;
};
interface User {
id: number;
name: string;
}
// Usage
function useApi<T>(): ApiResponse<T> {
return {
data: null,
error: null,
loading: false
};
}
const userResponse = useApi<User>();
State Management
Create type-safe state management:
type Action<T extends string, P = void> = P extends void
? { type: T }
: { type: T; payload: P };
type UserState = {
user: User | null;
loading: boolean;
error: string | null;
};
type UserAction =
| Action<'SET_USER', User>
| Action<'CLEAR_USER'>
| Action<'SET_ERROR', string>
| Action<'SET_LOADING', boolean>;
function reducer(state: UserState, action: UserAction): UserState {
switch (action.type) {
case 'SET_USER':
return { ...state, user: action.payload, error: null };
case 'CLEAR_USER':
return { ...state, user: null };
case 'SET_ERROR':
return { ...state, error: action.payload };
case 'SET_LOADING':
return { ...state, loading: action.payload };
}
}
Best Practices
- Use descriptive type names that reflect their purpose
- Combine utility types to create more complex types
- Leverage type inference when possible
- Document complex type manipulations
- Use type aliases for commonly used utility type combinations
Type Safety with Generics
Implement type-safe generic functions:
function createContainer<T>() {
return {
item: null as T | null,
setItem(value: T) {
this.item = value;
},
getItem(): T | null {
return this.item;
}
};
}
const numberContainer = createContainer<number>();
numberContainer.setItem(42); // OK
numberContainer.setItem('hello'); // Error!
By mastering these TypeScript utility types and their combinations, you can create more robust and maintainable codebases. Remember to use these tools judiciously and document complex type manipulations to help other developers understand your code better.