TypeScript Utility Types: From Partial to Awaited
Master TypeScript utility types with practical examples. Learn Partial, Required, Pick, Omit, Record, Awaited, and more — with real-world use cases that make your code type-safe and maintainable.
TypeScript Utility Types: From Partial to Awaited
TL;DR: TypeScript utility types are pre-built type transformations that save you from writing complex type logic from scratch. They’re like power tools for your type system — use them to make your code more maintainable and type-safe.
Ever found yourself writing the same type transformations over and over? Or copying properties from one type to create another?
TypeScript utility types are here to save the day. They’re built-in helpers that transform existing types into new ones, making your codebase more maintainable and your types more expressive.
Let’s dive into the most useful ones with real-world examples you’ll actually use.
What Are Utility Types?
Utility types are type-level functions that take one or more types and return a new type. Think of them as:
- Transformers → They modify existing types
- Composers → They combine types in useful ways
- Time-savers → They eliminate repetitive type definitions
Instead of manually creating types, you use these helpers to derive them automatically.
// Manual type definition (tedious)
type UserUpdate = {
name?: string;
email?: string;
age?: number;
};
// Using utility types (clean)
type UserUpdate = Partial<User>;The Essential Utility Types
1. Partial<T> — Make Everything Optional
What it does: Makes all properties of a type optional.
type User = {
name: string;
email: string;
age: number;
};
type UserUpdate = Partial<User>;
// Equivalent to:
// {
// name?: string;
// email?: string;
age?: number;
// }Real-world use case: Update operations where you only want to change specific fields.
async function updateUser(id: string, updates: Partial<User>) {
// Only update provided fields
const existing = await getUser(id);
return { ...existing, ...updates };
}
// All valid
updateUser("123", { name: "John" });
updateUser("123", { email: "new@email.com" });
updateUser("123", { name: "John", age: 30 });Pro tip: Combine Partial with Pick to make only specific fields optional:
type UserNameUpdate = Partial<Pick<User, 'name' | 'email'>>;2. Required<T> — Make Everything Required
What it does: Makes all optional properties required (opposite of Partial).
type Config = {
apiKey?: string;
timeout?: number;
retries?: number;
};
type StrictConfig = Required<Config>;
// {
// apiKey: string;
// timeout: number;
// retries: number;
// }Real-world use case: Enforcing all configuration values are provided.
function initializeApp(config: Required<Config>) {
// TypeScript guarantees all fields are present
console.log(config.apiKey); // No undefined check needed
console.log(config.timeout);
console.log(config.retries);
}
// TypeScript error
initializeApp({ apiKey: "key" });
// Valid
initializeApp({
apiKey: "key",
timeout: 5000,
retries: 3
});3. Readonly<T> — Make Everything Immutable
What it does: Makes all properties read-only.
type User = {
name: string;
email: string;
};
type ImmutableUser = Readonly<User>;
// {
// readonly name: string;
// readonly email: string;
// }
const user: ImmutableUser = { name: "John", email: "john@example.com" };
user.name = "Jane"; // TypeScript error: Cannot assign to 'name' because it is a read-only propertyReal-world use case: Preventing accidental mutations in React state or API responses.
type ApiResponse<T> = Readonly<{
data: T;
status: number;
timestamp: number;
}>;
function handleResponse(response: ApiResponse<User>) {
// Safe: can't accidentally mutate
console.log(response.data);
// response.data = {}; // Error
}Note: Readonly is shallow. For deep immutability, you’d need recursive types or libraries like ts-toolbelt.
4. Pick<T, K> — Select Specific Properties
What it does: Creates a type by picking specific properties from another type.
type User = {
id: string;
name: string;
email: string;
age: number;
role: string;
};
type UserProfile = Pick<User, 'name' | 'email' | 'age'>;
// {
// name: string;
// email: string;
// age: number;
// }Real-world use case: Creating view models or DTOs (Data Transfer Objects).
// API returns full user object
async function getUser(id: string): Promise<User> {
// ... fetch from API
}
// But we only want to display name and email
function displayUser(user: Pick<User, 'name' | 'email'>) {
return `${user.name} (${user.email})`;
}
const fullUser = await getUser("123");
displayUser(fullUser); // Works! TypeScript knows it has name and email5. Omit<T, K> — Exclude Specific Properties
What it does: Creates a type by removing specific properties (opposite of Pick).
type User = {
id: string;
name: string;
email: string;
password: string; // Sensitive!
createdAt: Date;
};
type PublicUser = Omit<User, 'password' | 'createdAt'>;
// {
// id: string;
// name: string;
// email: string;
// }Real-world use case: Removing sensitive data before sending to client.
function sanitizeUser(user: User): Omit<User, 'password' | 'internalId'> {
const { password, internalId, ...safe } = user;
return safe; // Type-safe removal
}
const user = await getUserFromDB("123");
const publicUser = sanitizeUser(user);
// publicUser.password // TypeScript error: Property doesn't existSecurity tip: Always use Omit when removing sensitive fields. TypeScript will catch any accidental leaks at compile time.
6. Record<K, T> — Create Object Types
What it does: Creates an object type with specific keys and values.
type Status = 'pending' | 'approved' | 'rejected';
type StatusConfig = Record<Status, { color: string; icon: string }>;
// {
// pending: { color: string; icon: string };
// approved: { color: string; icon: string };
// rejected: { color: string; icon: string };
// }
const config: StatusConfig = {
pending: { color: 'yellow', icon: '⏳' },
approved: { color: 'green', icon: '✅' },
rejected: { color: 'red', icon: '❌' }
};Real-world use case: Configuration objects, enums with metadata, or API route handlers.
type HttpMethod = 'GET' | 'POST' | 'PUT' | 'DELETE';
type RouteHandlers = Record<HttpMethod, (req: Request) => Response>;
const api: RouteHandlers = {
GET: (req) => new Response('Get handler'),
POST: (req) => new Response('Post handler'),
PUT: (req) => new Response('Put handler'),
DELETE: (req) => new Response('Delete handler')
// Missing any method? TypeScript error!
};7. Exclude<T, U> — Remove Types from Union
What it does: Removes specific types from a union type.
type AllStatus = 'pending' | 'approved' | 'rejected' | 'draft';
type ActiveStatus = Exclude<AllStatus, 'draft'>;
// 'pending' | 'approved' | 'rejected'Real-world use case: Filtering out specific states or types.
type EventType = 'click' | 'hover' | 'focus' | 'blur' | 'error';
type UserInteraction = Exclude<EventType, 'error'>;
// 'click' | 'hover' | 'focus' | 'blur'
function trackInteraction(event: UserInteraction) {
// Only user interactions, not errors
analytics.track(event);
}8. Extract<T, U> — Keep Only Matching Types
What it does: Keeps only types from a union that match another type (opposite of Exclude).
type AllStatus = 'pending' | 'approved' | 'rejected' | 'draft';
type FinalStatus = Extract<AllStatus, 'approved' | 'rejected'>;
// 'approved' | 'rejected'Real-world use case: Extracting specific types from unions.
type ApiResponse =
| { status: 'success'; data: User }
| { status: 'error'; message: string }
| { status: 'loading' };
type SuccessResponse = Extract<ApiResponse, { status: 'success' }>;
// { status: 'success'; data: User }
function handleSuccess(response: SuccessResponse) {
// TypeScript knows data exists
console.log(response.data);
}9. NonNullable<T> — Remove Null and Undefined
What it does: Removes null and undefined from a type.
type MaybeString = string | null | undefined;
type DefiniteString = NonNullable<MaybeString>;
// stringReal-world use case: Filtering out null/undefined from arrays or function returns.
function getUsers(): (User | null)[] {
// Some users might be null
return [user1, null, user2, null, user3];
}
function getValidUsers(): NonNullable<User>[] {
return getUsers().filter((user): user is NonNullable<typeof user> =>
user !== null
);
// Return type is User[], not (User | null)[]
}10. Parameters<T> — Extract Function Parameters
What it does: Extracts the parameter types from a function type.
function createUser(name: string, email: string, age: number) {
return { name, email, age };
}
type CreateUserParams = Parameters<typeof createUser>;
// [string, string, number]Real-world use case: Creating type-safe wrappers or middleware.
async function createUser(name: string, email: string, age: number) {
// ... create user logic
}
// Wrapper that logs before calling
function loggedCreateUser(...args: Parameters<typeof createUser>) {
console.log('Creating user with:', args);
return createUser(...args);
}
// Type-safe: args match createUser's parameters
loggedCreateUser("John", "john@example.com", 30);Advanced example: Type-safe API route handlers.
type Handler = (req: Request, res: Response) => Promise<void>;
function withAuth(handler: Handler) {
return async (req: Request, res: Response) => {
// Check authentication
if (!isAuthenticated(req)) {
return res.status(401).json({ error: 'Unauthorized' });
}
return handler(req, res);
};
}
// Usage
const protectedHandler = withAuth(async (req, res) => {
// TypeScript knows req and res types
const userId = req.user.id; // Type-safe
});11. ReturnType<T> — Extract Return Type
What it does: Extracts the return type from a function type.
function getUser(id: string): Promise<User> {
return fetch(`/api/users/${id}`).then(r => r.json());
}
type UserPromise = ReturnType<typeof getUser>;
// Promise<User>Real-world use case: Deriving types from existing functions.
async function fetchUser(id: string) {
const response = await fetch(`/api/users/${id}`);
return response.json() as User;
}
type FetchedUser = Awaited<ReturnType<typeof fetchUser>>;
// User (after unwrapping Promise)
async function processUser(id: string) {
const user: FetchedUser = await fetchUser(id);
// TypeScript knows it's User, not Promise<User>
console.log(user.name);
}12. Awaited<T> — Unwrap Promises
What it does: Unwraps nested promises to get the final resolved type.
type UserPromise = Promise<User>;
type UserData = Awaited<UserPromise>;
// User
// Awaited also handles nested promises
type NestedPromise = Promise<Promise<User>>;
type Unwrapped = Awaited<NestedPromise>;
// User (unwraps both Promise layers)Real-world use case: Working with async functions and their return types.
async function fetchUser(id: string): Promise<User> {
// ... fetch logic
}
async function fetchUserWithCache(id: string): Promise<User> {
// Returns cached user or fetches new one
const cached = cache.get(id);
return cached ?? fetchUser(id);
}
type CachedUser = Awaited<ReturnType<typeof fetchUserWithCache>>;
// User (unwraps Promise)
async function displayUser(id: string) {
const user: CachedUser = await fetchUserWithCache(id);
// TypeScript knows it's User
console.log(user.name);
}Pro tip: Awaited was introduced in TypeScript 4.5. Before that, you’d use ReturnType with conditional types. It’s especially useful with ReturnType:
type AsyncResult = Awaited<ReturnType<typeof asyncFunction>>;Combining Utility Types
The real power comes from combining utility types:
Example 1: Partial Update with Specific Fields
type User = {
id: string;
name: string;
email: string;
password: string;
role: string;
};
// Only allow updating name and email, and make them optional
type UserUpdate = Partial<Pick<User, 'name' | 'email'>>;
// {
// name?: string;
// email?: string;
// }Example 2: Read-only Public API
type User = {
id: string;
name: string;
email: string;
password: string;
internalNotes: string;
};
// Public user: read-only, without sensitive fields
type PublicUser = Readonly<Omit<User, 'password' | 'internalNotes'>>;
// {
// readonly id: string;
// readonly name: string;
// readonly email: string;
// }Example 3: Type-Safe Form State
type UserForm = {
name: string;
email: string;
age: number;
};
// Form errors: same keys, but values are strings (error messages)
type FormErrors = Partial<Record<keyof UserForm, string>>;
// {
// name?: string;
// email?: string;
// age?: string;
// }
function validateForm(form: UserForm): FormErrors {
const errors: FormErrors = {};
if (!form.name) errors.name = "Name is required";
if (!form.email.includes('@')) errors.email = "Invalid email";
if (form.age < 0) errors.age = "Age must be positive";
return errors;
}Advanced Patterns
Pattern 1: Deep Partial (Recursive)
type DeepPartial<T> = {
[P in keyof T]?: T[P] extends object ? DeepPartial<T[P]> : T[P];
};
type User = {
profile: {
name: string;
bio: string;
};
settings: {
theme: string;
};
};
type UserUpdate = DeepPartial<User>;
// {
// profile?: {
// name?: string;
// bio?: string;
// };
// settings?: {
// theme?: string;
// };
// }Pattern 2: Make Specific Fields Required
type RequireFields<T, K extends keyof T> = T & Required<Pick<T, K>>;
type User = {
name?: string;
email?: string;
age?: number;
};
type UserWithEmail = RequireFields<User, 'email'>;
// {
// name?: string;
// email: string; // Required
// age?: number;
// }Pattern 3: Extract Async Function Return Type
type AsyncReturnType<T extends (...args: any) => Promise<any>> =
Awaited<ReturnType<T>>;
async function fetchData(): Promise<User> {
// ...
}
type Data = AsyncReturnType<typeof fetchData>;
// UserQuick Reference Table
| Utility Type | What It Does | Common Use Case |
|---|---|---|
Partial<T> | Makes all properties optional | Update operations |
Required<T> | Makes all properties required | Strict configuration |
Readonly<T> | Makes all properties readonly | Immutable data |
Pick<T, K> | Selects specific properties | View models, DTOs |
Omit<T, K> | Removes specific properties | Removing sensitive data |
Record<K, T> | Creates object with specific keys | Configuration objects |
Exclude<T, U> | Removes types from union | Filtering union types |
Extract<T, U> | Keeps matching types from union | Extracting specific types |
NonNullable<T> | Removes null/undefined | Filtering arrays |
Parameters<T> | Extracts function parameters | Type-safe wrappers |
ReturnType<T> | Extracts return type | Deriving types from functions |
Awaited<T> | Unwraps promises | Working with async functions |
Best Practices
-
Use utility types instead of manual types → They’re maintained by TypeScript and handle edge cases.
-
Combine utility types → Chain them for complex transformations.
-
Document complex type transformations → Add comments explaining why you’re using specific utilities.
-
Prefer composition over duplication → Derive types from existing ones rather than copying.
-
Use
Awaitedwith async functions → Always unwrap promises when extracting return types.
Summary
TypeScript utility types are powerful tools that help you:
- ✅ Reduce duplication → Derive types instead of copying
- ✅ Improve type safety → Catch errors at compile time
- ✅ Enhance maintainability → Types update automatically when base types change
- ✅ Express intent → Code becomes more self-documenting
Start using them in your next project, and you’ll wonder how you ever lived without them! 🚀
Next steps: Try implementing these utility types in your current project. Look for places where you’re manually creating types that could be derived using utility types instead.