Understanding TypeScript utility types
You've probably written the same type definition three different ways. You have a User type for your database, a CreateUserRequest that's the same but without the id, and an UpdateUserRequest that makes everything optional. Now when you add a new field, you're updating it in three places and hoping you didn't miss anything.
This is exactly what TypeScript utility types solve. These are part of TypeScript's broader ecosystem of features that help you write safer, more maintainable code. Instead of duplicating type definitions, you can transform existing types on the fly. Make properties optional, required, or read-only. Extract specific fields. Remove others. Even manipulate string literal types.
Here's a complete reference of TypeScript's built-in utility types:
| Utility Type | Description |
|---|---|
Partial<T> | Constructs a type with all properties of T set to optional. |
Required<T> | Constructs a type with all properties of T set to required. |
Readonly<T> | Constructs a type with all properties of T marked as readonly. |
Record<K, T> | Constructs an object type whose keys are K and values are T. |
Pick<T, K> | Constructs a type by picking a set of properties K from T. |
Omit<T, K> | Constructs a type by omitting a set of properties K from T. |
Exclude<T, U> | Constructs a type by excluding from T all union members that are assignable to U. |
Extract<T, U> | Constructs a type by extracting from T all union members that are assignable to U. |
NonNullable<T> | Constructs a type by excluding null and undefined from T. |
Parameters<T> | Constructs a tuple type from the types of the parameters of a function type T. |
ConstructorParameters<T> | Constructs a tuple type from the types of the parameters of a constructor function type T. |
ReturnType<T> | Constructs a type consisting of the return type of a function type T. |
InstanceType<T> | Constructs a type consisting of the instance type of a constructor function type T. |
ThisParameterType<T> | Extracts the type of the this parameter for a function type T, if present. |
OmitThisParameter<T> | Constructs a function type with the this parameter removed from T. |
ThisType<T> | A marker type that can be used to specify the type of this in an object literal. |
Awaited<T> | Constructs a type by resolving the awaited type of a Promise-like type T. |
Uppercase<S> | Converts a string literal type S to uppercase. |
Lowercase<S> | Converts a string literal type S to lowercase. |
Capitalize<S> | Converts the first character of a string literal type S to uppercase. |
Uncapitalize<S> | Converts the first character of a string literal type S to lowercase. |
To learn more about each utility type, click the link to each respective type in the table above. In this article, we'll walk through practical ways to use these TypeScript utility types in real projects.
Refactoring code with utility types
TypeScript utility types like Partial, Pick, and Omit eliminate repetitive type definitions. Let's look at how each one solves specific problems.
Partial makes all properties optional, which is perfect for update operations where you only want to change specific fields:
interface Product {
id: string;
name: string;
price: number;
description: string;
inStock: boolean;
}
// PATCH /api/products/:id
async function updateProduct(id: string, updates: Partial<Product>) {
// Only the fields you're actually changing need to be passed
// TypeScript still validates that field names and types are correct
return await db.products.update(id, updates);
}
// Valid: update just the price
updateProduct("prod_123", { price: 29.99 });
// Valid: update multiple fields
updateProduct("prod_123", { price: 29.99, inStock: true });
Pick creates a new type by selecting specific properties. Use this when you need a subset of fields from a larger type:
interface User {
id: string;
email: string;
passwordHash: string;
firstName: string;
lastName: string;
createdAt: Date;
lastLogin: Date;
}
// For your API response, only expose safe fields
type UserProfile = Pick<User, "id" | "email" | "firstName" | "lastName">;
// TypeScript ensures you only access the picked fields
function formatUserDisplay(user: UserProfile) {
return `${user.firstName} ${user.lastName} (${user.email})`;
// user.passwordHash // Error: Property doesn't exist on UserProfile
}
Omit does the opposite by removing specific properties. It's cleaner when you want most fields but need to exclude a few:
interface User {
id: string;
email: string;
passwordHash: string;
firstName: string;
lastName: string;
}
// POST /api/users - id is auto-generated, no password in the request
type CreateUserRequest = Omit<User, "id" | "passwordHash"> & {
password: string; // Plain password for the request
};
async function registerUser(data: CreateUserRequest) {
const passwordHash = await hashPassword(data.password);
// Create user with generated id and hashed password
}
Pick vs Omit: Which one should you use?
Choosing between Pick and Omit depends on how many properties you're selecting versus excluding.
Use Pick when:
- You need only a small subset of properties from a large type
- You want to be explicit about which fields are allowed
- Adding new properties to the base type shouldn't automatically include them
interface DatabaseUser {
id: string;
email: string;
passwordHash: string;
salt: string;
emailVerified: boolean;
twoFactorSecret: string | null;
createdAt: Date;
updatedAt: Date;
lastLogin: Date;
// ... 10 more internal fields
}
// Only expose these specific fields publicly
type PublicUser = Pick<DatabaseUser, "id" | "email" | "emailVerified">;
Use Omit when:
- You want most properties except a few sensitive ones
- The excluded fields are the minority
- New properties added to the base type should be included by default
interface Article {
id: string;
title: string;
content: string;
authorId: string;
publishedAt: Date;
viewCount: number;
}
// Everything except the id (auto-generated on create)
type CreateArticleInput = Omit<Article, "id" | "viewCount">;
Rule of thumb: If you're selecting fewer than half the properties, use Pick. If you're excluding fewer than half, use Omit.
Enforcing stricter types for safety
Utility types like Readonly and Required add constraints that catch bugs at compile time.
Readonly prevents accidental mutations, which is especially useful for configuration objects or data you pass to functions that shouldn't modify it:
interface AppConfig {
apiUrl: string;
timeout: number;
retryAttempts: number;
}
const config: Readonly<AppConfig> = {
apiUrl: "https://api.example.com",
timeout: 5000,
retryAttempts: 3
};
// TypeScript prevents this at compile time
// config.timeout = 10000; // Error: Cannot assign to 'timeout' because it is a read-only property
function makeRequest(cfg: Readonly<AppConfig>) {
// Function signature guarantees we won't modify the config
// Callers can trust their config object stays unchanged
}
Required ensures all properties have values, useful when you need to enforce completeness:
interface UserPreferences {
theme?: "light" | "dark";
notifications?: boolean;
language?: string;
}
// During account setup, require all preferences to be set
function initializeAccount(prefs: Required<UserPreferences>) {
// TypeScript ensures theme, notifications, and language are all present
saveToDatabase(prefs);
}
initializeAccount({
theme: "dark",
notifications: true,
language: "en"
}); // Valid
// initializeAccount({ theme: "dark" }); // Error: missing properties
Building type-safe mappings with Record
Record creates objects where keys and values follow specific types. It's perfect for lookup tables, permission maps, or any key-value structure:
type UserRole = "admin" | "editor" | "viewer";
// Map each role to its permissions
const permissions: Record<UserRole, string[]> = {
admin: ["read", "write", "delete", "manage_users"],
editor: ["read", "write"],
viewer: ["read"]
};
// TypeScript ensures:
// 1. All roles are defined
// 2. Each value is a string array
// 3. No extra keys exist
function checkPermission(role: UserRole, action: string): boolean {
return permissions[role].includes(action);
}
You can also use Record for dynamic string keys:
// Cache API responses by endpoint
const apiCache: Record<string, { data: unknown; timestamp: number }> = {};
function cacheResponse(endpoint: string, data: unknown) {
apiCache[endpoint] = {
data,
timestamp: Date.now()
};
}
Working with union types using Extract and Exclude
Exclude and Extract filter union types, letting you remove or keep specific members.
Extract pulls out matching types from a union:
type AllEvents =
| { type: "click"; x: number; y: number }
| { type: "keypress"; key: string }
| { type: "focus" }
| { type: "blur" };
// Get only events with additional data
type EventsWithData = Extract<AllEvents, { type: "click" | "keypress" }>;
// Result: { type: "click"; x: number; y: number } | { type: "keypress"; key: string }
function logEventData(event: EventsWithData) {
// TypeScript knows these events have extra properties
if (event.type === "click") {
console.log(`Clicked at ${event.x}, ${event.y}`);
}
}
Exclude removes specific types from a union:
type Status = "pending" | "processing" | "complete" | "failed";
// Remove terminal states to get active statuses
type ActiveStatus = Exclude<Status, "complete" | "failed">;
// Result: "pending" | "processing"
function canRetry(status: ActiveStatus): boolean {
// Only active statuses are allowed here
return status === "pending";
}
Removing null and undefined with NonNullable
NonNullable strips null and undefined from a type, which is helpful after you've validated that a value exists:
type ApiResponse = {
user: User | null;
error: string | undefined;
};
function processUser(response: ApiResponse) {
if (response.user === null) {
throw new Error("No user found");
}
// After the check, we know user is not null
// But TypeScript still thinks it could be
const user: NonNullable<typeof response.user> = response.user;
// Now user is typed as User, not User | null
console.log(user.email.toLowerCase()); // Safe to use
}
It's also useful for filtering arrays:
const values: (string | null | undefined)[] = ["hello", null, "world", undefined];
// Filter out null/undefined and update the type
const definedValues: NonNullable<typeof values[number]>[] =
values.filter((v): v is NonNullable<typeof v> => v != null);
// Type: string[]
Combining utility types for complex transformations
You can chain utility types to create more sophisticated type transformations. This is powerful but can hurt readability if overused.
Common pattern: Partial updates with selected fields
interface Product {
id: string;
name: string;
price: number;
description: string;
category: string;
inStock: boolean;
createdAt: Date;
updatedAt: Date;
}
// Allow partial updates, but only for editable fields
type UpdateProductInput = Partial<Pick<Product, "name" | "price" | "description" | "category" | "inStock">>;
function updateProduct(id: string, updates: UpdateProductInput) {
// Can update name, price, etc., but not id or timestamps
// All fields are optional
}
updateProduct("prod_123", { price: 29.99 }); // Valid
updateProduct("prod_123", { name: "New Name", inStock: true }); // Valid
// updateProduct("prod_123", { id: "new_id" }); // Error: id not in type
Another pattern: Required subset of optional properties
interface FormData {
email?: string;
phone?: string;
address?: string;
name?: string;
}
// Require email and name, keep others optional
type RegistrationForm = Required<Pick<FormData, "email" | "name">> &
Pick<FormData, "phone" | "address">;
const registration: RegistrationForm = {
email: "user@example.com", // Required
name: "John Doe", // Required
phone: "555-0100" // Optional
};
When to avoid chaining: If you need more than two utility types or find yourself explaining the type in a comment, create a named intermediate type instead:
// Hard to read
type ComplexType = Readonly<Required<Pick<Partial<User>, "id" | "email">>>;
// Better: break it down
type UserEmailInfo = Pick<User, "id" | "email">;
type RequiredEmailInfo = Required<UserEmailInfo>;
type ImmutableEmailInfo = Readonly<RequiredEmailInfo>;
Where developers get stuck
Here are the most common mistakes when working with utility types:
Misusing Partial to bypass type checks
Don't use Partial just to make TypeScript stop complaining about missing properties. It has a legitimate purpose (partial updates), but it's not a quick fix for incomplete data:
interface User {
id: string;
email: string;
name: string;
}
// Bad: using Partial to allow incomplete user objects
function createUser(data: Partial<User>) {
// What if id, email, or name is missing?
// You've lost type safety
return saveToDatabase(data); // Potential runtime error
}
// Good: be explicit about what's optional
interface CreateUserInput {
email: string; // Required
name: string; // Required
// id is omitted because it's auto-generated
}
function createUser(data: CreateUserInput) {
const user: User = {
id: generateId(),
...data
};
return saveToDatabase(user);
}
Allowing incomplete objects with Partial during creation
When you use Partial<T> for a function parameter, TypeScript will accept an empty object {}. This often isn't what you want:
interface Config {
apiKey: string;
endpoint: string;
timeout: number;
}
// Bad: this accepts {} as valid input
function initialize(config: Partial<Config>) {
// What if config is {}? Your app will break at runtime
fetch(config.endpoint, { // config.endpoint might be undefined!
headers: { "X-API-Key": config.apiKey }
});
}
initialize({}); // TypeScript allows this, but it'll crash
// Good: require at minimum the critical fields
function initialize(config: Pick<Config, "apiKey" | "endpoint"> & Partial<Pick<Config, "timeout">>) {
const fullConfig = {
timeout: 5000, // Default value
...config
};
fetch(fullConfig.endpoint, {
headers: { "X-API-Key": fullConfig.apiKey }
});
}
Over-nesting utility types
Deeply nested utility types become difficult to understand and maintain:
// Hard to read
type UpdateRequest = Partial<Pick<Omit<User, "id">, "email" | "name">>;
// Better: use intermediate types with descriptive names
type EditableUserFields = Omit<User, "id">;
type UserContactInfo = Pick<EditableUserFields, "email" | "name">;
type UpdateRequest = Partial<UserContactInfo>;
Not understanding that Readonly is compile-time only
Readonly prevents modifications in TypeScript, but JavaScript can still mutate the values. It's a type-level protection, not a runtime guarantee:
const config: Readonly<AppConfig> = {
apiUrl: "https://api.example.com"
};
// TypeScript error
// config.apiUrl = "https://malicious.com";
// But at runtime, JavaScript allows this
(config as any).apiUrl = "https://malicious.com"; // Works in JavaScript
// For true immutability, use Object.freeze()
const frozenConfig = Object.freeze(config);
Using Omit with typos in property names
Omit doesn't verify that the properties you're omitting actually exist, which can lead to subtle bugs:
interface User {
id: string;
email: string;
name: string;
}
// Typo: "emial" instead of "email"
// TypeScript doesn't catch this!
type UserWithoutEmail = Omit<User, "emial">;
// Result still has all three properties, not what you wanted
const user: UserWithoutEmail = {
id: "123",
email: "user@example.com", // Still here because of the typo
name: "John"
};
To catch these errors, consider using Pick instead when you're selecting only a few properties. Or use a stricter type-checking setup that warns about non-existent keys.
Working with conditional types
Conditional types let you create types based on conditions, similar to ternary operators:
type IsString<T> = T extends string ? true : false;
type Test1 = IsString<"hello">; // true
type Test2 = IsString<123>; // false
This becomes powerful when combined with utility types:
// Extract function types from an object
type FunctionPropertyNames<T> = {
[K in keyof T]: T[K] extends Function ? K : never;
}[keyof T];
interface User {
id: string;
name: string;
save: () => void;
delete: () => void;
}
type UserMethods = FunctionPropertyNames<User>; // "save" | "delete"
Using mapped types with utility types
Mapped types iterate over an existing type to create a new one:
type MappedType<T> = {
[P in keyof T]: T[P];
};
interface OriginalType {
a: string;
b: number;
}
type MappedTypeResult = MappedType<OriginalType>;
You can combine mapped types with conditionals for more advanced transformations:
// Make all string properties optional, keep others required
type PartialStrings<T> = {
[K in keyof T]: T[K] extends string ? T[K] | undefined : T[K];
};
interface Product {
id: number;
name: string;
description: string;
price: number;
}
type FlexibleProduct = PartialStrings<Product>;
// Result: {
// id: number;
// name: string | undefined;
// description: string | undefined;
// price: number;
// }
When not to use utility types
Utility types are powerful, but they're not always the right choice. Here's when to skip them:
Don't use utility types when a simple interface is clearer:
// Over-engineered
type UserResponse = Pick<User, "id" | "name" | "email">;
// Better: just define what you need
interface UserResponse {
id: string;
name: string;
email: string;
}
The utility type version only helps if UserResponse needs to stay in sync with User. If they're independent types, define them independently.
Don't use utility types for one-off transformations:
// Overkill for a single use
type TempType = Omit<SomeComplexType, "field1" | "field2" | "field3">;
function doSomething(data: TempType) { }
// Better: just define the parameter inline
function doSomething(data: {
fieldA: string;
fieldB: number;
}) { }
Don't chain utility types when the result is unclear:
// What does this even mean?
type Mysterious = Readonly<Partial<Required<Pick<User, "email" | "name">>>>;
// Better: write out the type you actually want
type UserEmailAndName = Readonly<{
email: string;
name: string;
}>;
Use utility types when they provide real value: automatic synchronization with a base type, reducing duplication, or making intent clearer. If you're just making the code more clever without adding value, skip them.
Key takeaways
TypeScript utility types help you transform existing types without duplicating code. Here's what to remember:
- Use
Partialfor update operations where only some fields change, but watch out for accepting empty objects - Choose
Pickwhen selecting a small subset of properties,Omitwhen excluding a few - Combine utility types for complex scenarios, but use intermediate named types when nesting gets deep
Readonlyprevents modifications at compile time, not runtime—useObject.freeze()for true immutabilityRecordcreates type-safe key-value mappings for lookup tables and caches- Don't use utility types just to make TypeScript happy. They should make your code more maintainable
Start with the simple ones like Partial and Pick. As you get comfortable, you'll recognize patterns where Record, Extract, or combined utility types solve problems more elegantly than manual type definitions.