Lesson 5.4: Utility Types
Duration: 60 minutes
Learning Objectives
By the end of this lesson, you will be able to:
- Use
Partial,Required,Readonly, andPick - Transform types with
Omit,Record, andExtract - Work with function types using
Parameters,ReturnType - Combine utility types for complex transformations
- Know when to use each utility type
What Are Utility Types?
Utility types are built-in generic types that transform other types. They save you from writing repetitive type definitions and make your code more expressive.
interface User {
id: number;
name: string;
email: string;
}
// Without utility types - verbose
interface PartialUser {
id?: number;
name?: string;
email?: string;
}
// With utility types - concise
type PartialUser = Partial<User>;
Partial
Makes all properties optional:
interface User {
id: number;
name: string;
email: string;
}
type PartialUser = Partial<User>;
// Equivalent to:
// {
// id?: number;
// name?: string;
// email?: string;
// }
// Perfect for update functions
function updateUser(id: number, updates: Partial<User>): User {
const user = getUserById(id);
return { ...user, ...updates };
}
// Only update what you need
updateUser(1, { name: 'New Name' });
updateUser(2, { email: 'new@example.com' });
Common Use Case: Form State
interface FormData {
username: string;
password: string;
email: string;
}
// Form state starts empty
const formState: Partial<FormData> = {};
// Fill in as user types
formState.username = 'alice';
formState.email = 'alice@example.com';
// Check if complete before submit
function isComplete(form: Partial<FormData>): form is FormData {
return form.username !== undefined && form.password !== undefined && form.email !== undefined;
}
Required
Makes all properties required (opposite of Partial):
interface Config {
host?: string;
port?: number;
secure?: boolean;
}
type RequiredConfig = Required<Config>;
// Equivalent to:
// {
// host: string;
// port: number;
// secure: boolean;
// }
// Ensure all config values are set
function initServer(config: Required<Config>) {
console.log(`Starting server on ${config.host}:${config.port}`);
console.log(`Secure: ${config.secure}`);
}
// Must provide all values
initServer({
host: 'localhost',
port: 3000,
secure: true,
});
Readonly
Makes all properties readonly:
interface User {
id: number;
name: string;
email: string;
}
type ReadonlyUser = Readonly<User>;
// Equivalent to:
// {
// readonly id: number;
// readonly name: string;
// readonly email: string;
// }
const user: ReadonlyUser = {
id: 1,
name: 'Alice',
email: 'alice@example.com',
};
// Error: Cannot assign to 'name' because it is a read-only property
// user.name = "Bob";
// Perfect for immutable state
function freeze<T>(obj: T): Readonly<T> {
return Object.freeze(obj) as Readonly<T>;
}
Deep Readonly
Note that Readonly is shallow. For nested objects:
type DeepReadonly<T> = {
readonly [K in keyof T]: T[K] extends object ? DeepReadonly<T[K]> : T[K];
};
interface Company {
name: string;
address: {
street: string;
city: string;
};
}
type ReadonlyCompany = DeepReadonly<Company>;
// All nested properties are also readonly
Pick<T, K>
Creates a type with only the specified properties:
interface User {
id: number;
name: string;
email: string;
password: string;
createdAt: Date;
}
type PublicUser = Pick<User, 'id' | 'name' | 'email'>;
// Equivalent to:
// {
// id: number;
// name: string;
// email: string;
// }
// Expose only safe fields
function getPublicProfile(user: User): PublicUser {
return {
id: user.id,
name: user.name,
email: user.email,
};
}
Use Case: API Responses
interface Product {
id: number;
name: string;
description: string;
price: number;
costPrice: number; // Internal only
supplier: string; // Internal only
}
// Public API response
type ProductResponse = Pick<Product, 'id' | 'name' | 'description' | 'price'>;
function toProductResponse(product: Product): ProductResponse {
return {
id: product.id,
name: product.name,
description: product.description,
price: product.price,
};
}
Omit<T, K>
Creates a type without the specified properties (opposite of Pick):
interface User {
id: number;
name: string;
email: string;
password: string;
}
type SafeUser = Omit<User, 'password'>;
// Equivalent to:
// {
// id: number;
// name: string;
// email: string;
// }
// Also works with multiple keys
type UserWithoutSensitive = Omit<User, 'password' | 'email'>;
// { id: number; name: string; }
Use Case: Creating New Records
interface DatabaseRecord {
id: number;
createdAt: Date;
updatedAt: Date;
name: string;
value: number;
}
// For creating new records (id and timestamps are auto-generated)
type NewRecord = Omit<DatabaseRecord, 'id' | 'createdAt' | 'updatedAt'>;
// { name: string; value: number; }
function createRecord(data: NewRecord): DatabaseRecord {
return {
id: generateId(),
createdAt: new Date(),
updatedAt: new Date(),
...data,
};
}
createRecord({ name: 'Test', value: 42 });
Record<K, T>
Creates an object type with keys of type K and values of type T:
// Simple string keys
type Roles = Record<string, boolean>;
const userRoles: Roles = {
admin: true,
editor: false,
viewer: true,
};
// Union type keys
type Status = 'pending' | 'active' | 'completed';
type StatusColors = Record<Status, string>;
const colors: StatusColors = {
pending: 'yellow',
active: 'green',
completed: 'blue',
};
Use Case: Lookup Tables
type HttpMethod = 'GET' | 'POST' | 'PUT' | 'DELETE';
interface HandlerConfig {
handler: () => void;
requiresAuth: boolean;
}
type RouteHandlers = Record<HttpMethod, HandlerConfig>;
const handlers: RouteHandlers = {
GET: { handler: () => {}, requiresAuth: false },
POST: { handler: () => {}, requiresAuth: true },
PUT: { handler: () => {}, requiresAuth: true },
DELETE: { handler: () => {}, requiresAuth: true },
};
Use Case: Grouping Data
interface User {
id: number;
name: string;
department: string;
}
type UsersByDepartment = Record<string, User[]>;
function groupByDepartment(users: User[]): UsersByDepartment {
return users.reduce((groups, user) => {
const dept = user.department;
if (!groups[dept]) {
groups[dept] = [];
}
groups[dept].push(user);
return groups;
}, {} as UsersByDepartment);
}
Extract<T, U>
Extracts types from T that are assignable to U:
type AllTypes = string | number | boolean | null | undefined;
type Primitives = Extract<AllTypes, string | number | boolean>;
// string | number | boolean
type Nullish = Extract<AllTypes, null | undefined>;
// null | undefined
Use Case: Filtering Union Types
type Event =
| { type: 'click'; x: number; y: number }
| { type: 'keypress'; key: string }
| { type: 'scroll'; position: number }
| { type: 'resize'; width: number; height: number };
// Extract events with coordinates
type PointEvent = Extract<Event, { x: number }>;
// { type: "click"; x: number; y: number }
// Extract events with a specific type
type ClickEvent = Extract<Event, { type: 'click' }>;
// { type: "click"; x: number; y: number }
Exclude<T, U>
Excludes types from T that are assignable to U (opposite of Extract):
type AllTypes = string | number | boolean | null | undefined;
type NonNullable = Exclude<AllTypes, null | undefined>;
// string | number | boolean
Use Case: Removing Specific Types
type Event =
| { type: 'click'; x: number; y: number }
| { type: 'keypress'; key: string }
| { type: 'scroll'; position: number };
// All events except click
type NonClickEvent = Exclude<Event, { type: 'click' }>;
// { type: "keypress"; key: string } | { type: "scroll"; position: number }
NonNullable
Removes null and undefined from a type:
type MaybeString = string | null | undefined;
type DefiniteString = NonNullable<MaybeString>;
// string
function processValue(value: string | null | undefined) {
if (value !== null && value !== undefined) {
const safe: NonNullable<typeof value> = value;
console.log(safe.toUpperCase());
}
}
Parameters
Extracts the parameter types of a function as a tuple:
function createUser(name: string, age: number, isAdmin: boolean) {
return { name, age, isAdmin };
}
type CreateUserParams = Parameters<typeof createUser>;
// [string, number, boolean]
// Use spread to call with array
const params: CreateUserParams = ['Alice', 30, false];
const user = createUser(...params);
Use Case: Wrapping Functions
function originalFunction(a: string, b: number): boolean {
return a.length > b;
}
function wrapper(
...args: Parameters<typeof originalFunction>
): ReturnType<typeof originalFunction> {
console.log('Calling with:', args);
return originalFunction(...args);
}
wrapper('hello', 3); // Logs: "Calling with: ["hello", 3]", returns true
ReturnType
Extracts the return type of a function:
function getUser() {
return {
id: 1,
name: 'Alice',
email: 'alice@example.com',
};
}
type User = ReturnType<typeof getUser>;
// { id: number; name: string; email: string; }
// Useful for inferring types from existing functions
async function fetchData() {
const response = await fetch('/api/data');
return response.json() as Promise<{ items: string[] }>;
}
type FetchResult = Awaited<ReturnType<typeof fetchData>>;
// { items: string[] }
Awaited
Extracts the type from a Promise:
type PromiseString = Promise<string>;
type ResolvedString = Awaited<PromiseString>;
// string
// Works with nested promises
type NestedPromise = Promise<Promise<number>>;
type ResolvedNested = Awaited<NestedPromise>;
// number
// Practical usage with async functions
async function fetchUser(): Promise<{ id: number; name: string }> {
return { id: 1, name: 'Alice' };
}
type User = Awaited<ReturnType<typeof fetchUser>>;
// { id: number; name: string }
Combining Utility Types
The real power comes from combining utility types:
Example 1: Partial Update with Pick
interface User {
id: number;
name: string;
email: string;
password: string;
settings: {
theme: string;
notifications: boolean;
};
}
// Only allow updating certain fields, and make them optional
type UserUpdate = Partial<Pick<User, 'name' | 'email' | 'settings'>>;
function updateUser(id: number, updates: UserUpdate) {
// Can update name, email, or settings - all optional
}
updateUser(1, { name: 'New Name' });
updateUser(1, { settings: { theme: 'dark', notifications: true } });
Example 2: Required Fields from Partial
interface FormData {
username?: string;
password?: string;
email?: string;
bio?: string;
}
// Some fields are required for submission
type SubmissionData = Required<Pick<FormData, 'username' | 'password' | 'email'>> &
Pick<FormData, 'bio'>;
// { username: string; password: string; email: string; bio?: string }
Example 3: Readonly API Responses
interface ApiResponse<T> {
data: T;
status: number;
message: string;
}
type ImmutableResponse<T> = Readonly<ApiResponse<Readonly<T>>>;
interface User {
id: number;
name: string;
}
type UserResponse = ImmutableResponse<User>;
// All properties are readonly, including nested User properties
Example 4: Omit and Record Together
interface Todo {
id: number;
title: string;
completed: boolean;
createdAt: Date;
}
// Create a record of todos by id, without the id field in the value
type TodoMap = Record<number, Omit<Todo, 'id'>>;
const todos: TodoMap = {
1: { title: 'Learn TypeScript', completed: true, createdAt: new Date() },
2: { title: 'Build a project', completed: false, createdAt: new Date() },
};
Quick Reference Table
| Utility Type | Purpose | Example |
|---|---|---|
Partial<T> |
Make all properties optional | Partial<User> |
Required<T> |
Make all properties required | Required<Config> |
Readonly<T> |
Make all properties readonly | Readonly<State> |
Pick<T, K> |
Select specific properties | Pick<User, "id" | "name"> |
Omit<T, K> |
Remove specific properties | Omit<User, "password"> |
Record<K, T> |
Create object type | Record<string, number> |
Extract<T, U> |
Extract matching types | Extract<A | B, A> |
Exclude<T, U> |
Remove matching types | Exclude<A | B, A> |
NonNullable<T> |
Remove null/undefined | NonNullable<string | null> |
Parameters<T> |
Get function parameters | Parameters<typeof fn> |
ReturnType<T> |
Get function return type | ReturnType<typeof fn> |
Awaited<T> |
Unwrap Promise type | Awaited<Promise<string>> |
Exercises
Exercise 1: API Types
Create types for a user API using utility types:
interface User {
id: number;
username: string;
email: string;
password: string;
createdAt: Date;
updatedAt: Date;
}
// Create these types using utility types:
// 1. CreateUserInput - for creating users (no id, dates, or password visible)
// 2. UpdateUserInput - for updating users (optional username, email)
// 3. PublicUser - for API responses (no password)
Solution
interface User {
id: number;
username: string;
email: string;
password: string;
createdAt: Date;
updatedAt: Date;
}
// For creating users
type CreateUserInput = Pick<User, 'username' | 'email' | 'password'>;
// For updating users
type UpdateUserInput = Partial<Pick<User, 'username' | 'email'>>;
// For API responses
type PublicUser = Omit<User, 'password'>;
// Test
const createData: CreateUserInput = {
username: 'alice',
email: 'alice@example.com',
password: 'secret123',
};
const updateData: UpdateUserInput = {
email: 'newemail@example.com',
};
function getPublicUser(user: User): PublicUser {
const { password, ...publicData } = user;
return publicData;
}
Exercise 2: Form Validation
Create a type system for form validation:
interface FormFields {
username: string;
email: string;
password: string;
confirmPassword: string;
}
// Create:
// 1. FormErrors - each field can have an error message or be undefined
// 2. FormTouched - track which fields have been touched (boolean for each)
// 3. ValidatedForm - a form where all fields are required and readonly
Solution
interface FormFields {
username: string;
email: string;
password: string;
confirmPassword: string;
}
// Each field can have an error message
type FormErrors = Partial<Record<keyof FormFields, string>>;
// Track touched state for each field
type FormTouched = Record<keyof FormFields, boolean>;
// Validated form is complete and immutable
type ValidatedForm = Readonly<Required<FormFields>>;
// Usage
const errors: FormErrors = {
email: 'Invalid email format',
password: 'Password too short',
};
const touched: FormTouched = {
username: true,
email: true,
password: false,
confirmPassword: false,
};
function validateForm(fields: Partial<FormFields>): ValidatedForm | null {
if (fields.username && fields.email && fields.password && fields.confirmPassword) {
return Object.freeze({
username: fields.username,
email: fields.email,
password: fields.password,
confirmPassword: fields.confirmPassword,
});
}
return null;
}
Exercise 3: Event Handler Types
Extract and use function types:
const eventHandlers = {
onClick: (event: { x: number; y: number }) => {
console.log(`Clicked at ${event.x}, ${event.y}`);
},
onKeyPress: (event: { key: string; code: number }) => {
console.log(`Pressed ${event.key}`);
},
onSubmit: (data: { formId: string; values: Record<string, string> }) => {
return { success: true, id: '123' };
},
};
// Create types for:
// 1. ClickEventParam - the parameter type of onClick
// 2. SubmitResult - the return type of onSubmit
// 3. AllHandlers - a type representing all handler functions
Solution
const eventHandlers = {
onClick: (event: { x: number; y: number }) => {
console.log(`Clicked at ${event.x}, ${event.y}`);
},
onKeyPress: (event: { key: string; code: number }) => {
console.log(`Pressed ${event.key}`);
},
onSubmit: (data: { formId: string; values: Record<string, string> }) => {
return { success: true, id: '123' };
},
};
// Parameter type of onClick
type ClickEventParam = Parameters<typeof eventHandlers.onClick>[0];
// { x: number; y: number }
// Return type of onSubmit
type SubmitResult = ReturnType<typeof eventHandlers.onSubmit>;
// { success: boolean; id: string }
// All handler types
type AllHandlers = typeof eventHandlers;
// Individual handler type
type OnClickHandler = AllHandlers['onClick'];
// (event: { x: number; y: number }) => void
// Use the extracted types
function triggerClick(param: ClickEventParam) {
eventHandlers.onClick(param);
}
triggerClick({ x: 100, y: 200 });
Key Takeaways
- Utility types transform types: They create new types from existing ones
- Partial/Required: Toggle optionality of all properties
- Readonly: Make properties immutable
- Pick/Omit: Select or exclude specific properties
- Record: Create object types with specific key and value types
- Extract/Exclude: Filter union types
- Parameters/ReturnType: Work with function types
- Combine them: Real power comes from chaining utility types
Resources
| Resource | Type | Description |
|---|---|---|
| TypeScript Handbook: Utility Types | Documentation | Complete list of utility types |
| TypeScript Utility Types Playground | Tool | Try utility types interactively |
| Type Challenges | Practice | Advanced type exercises |
Next Lesson
Now let us put everything together and build a complete generic storage system.