TypeScript's utility types are one of its most practical features and one of the most commonly overlooked. Rather than defining new types from scratch every time a variation of an existing type is needed, utility types provide a set of built-in transformations: make all fields optional, exclude certain keys, extract a return type from a function, and so on.
This guide covers the most useful utility types with specific examples and the situations where each one is the right tool.
What this covers:
PartialandRequiredfor optional and required field control
PickandOmitfor selecting and excluding fields
Readonlyfor immutable types
Recordfor key-value mappings
ReturnTypeandParametersfor function type extraction
NonNullablefor excluding null and undefinedCombining utility types for more complex transformations
The Base Type Used in Examples
Most examples in this guide use a common User type for consistency:
type User = {
id: number;
name: string;
email: string;
role: 'admin' | 'editor' | 'viewer';
};
Partial<Type>
Makes all properties of a type optional. Every field becomes field?: type rather than field: type.
const updateUser = (id: number, updates: Partial<User>): void => {
// updates may contain any subset of User fields
};
updateUser(1, { name: "Ada" }); // valid
updateUser(1, { email: "[email protected]" }); // valid
updateUser(1, { name: "Ada", role: "editor" }); // valid
This is most useful for update and patch endpoints where only a subset of fields is being modified. Without Partial, the function would require the full User object even for single-field updates.
Required<Type>
The inverse of Partial. Makes all properties required, removing any ? from the type definition.
type Config = {
cache?: boolean;
retries?: number;
timeout?: number;
};
type StrictConfig = Required<Config>;
// { cache: boolean; retries: number; timeout: number }
Useful when a type is defined with optional fields for flexibility during construction, but a function downstream needs to guarantee all fields are present before proceeding.
Pick<Type, Keys>
Creates a new type containing only the specified keys from an existing type.
type UserPreview = Pick<User, 'id' | 'name'>;
// { id: number; name: string }
A common use case is building view models or API response shapes that expose only the fields a particular consumer needs, without defining a separate interface manually.
function getUserSummaries(users: User[]): Pick<User, 'id' | 'name'>[] {
return users.map(({ id, name }) => ({ id, name }));
}
Omit<Type, Keys>
Creates a new type by removing the specified keys from an existing type. The counterpart to Pick.
type PublicUser = Omit<User, 'email'>;
// { id: number; name: string; role: 'admin' | 'editor' | 'viewer' }
Useful for stripping sensitive or internal fields before returning data to a client, or for creating a base type that will be extended with different field sets.
type NewUser = Omit<User, 'id'>;
// id is assigned by the database, not provided by the caller
Readonly<Type>
Makes all properties of a type read-only. Assignments to any property after initialization produce a type error.
const config: Readonly<Config> = {
cache: true,
retries: 3,
timeout: 5000,
};
config.retries = 5; // Error: Cannot assign to 'retries' because it is a read-only property
This is a compile-time enforcement only. Readonly does not use Object.freeze and does not prevent mutation at runtime. For deep immutability (nested objects), a recursive DeepReadonly type is needed, which TypeScript does not provide as a built-in but can be defined manually.
Record<Keys, Type>
Constructs a type with a set of keys mapped to a specified value type. Both the keys and the value type are explicit.
type Permission = 'read' | 'write' | 'delete';
type PermissionMap = Record<Permission, boolean>;
const userPermissions: PermissionMap = {
read: true,
write: true,
delete: false,
};
Record enforces that all keys in the union are present. Omitting delete from userPermissions would be a type error, which prevents accidentally incomplete mappings.
It also works with string or number as the key type for more open-ended maps:
type Cache = Record<string, User>;
ReturnType<Function>
Extracts the return type of a function type.
function getUser() {
return { id: 1, name: "Jane", role: "editor" as const };
}
type UserResult = ReturnType<typeof getUser>;
// { id: number; name: string; role: "editor" }
This is particularly useful when the return type of a function is complex or computed, and defining it separately would mean maintaining two things that must stay in sync. ReturnType keeps the type derived from the function automatically.
It also works with generic and async functions:
async function fetchUser(id: number) {
return { id, name: "Jane" };
}
type FetchResult = Awaited<ReturnType<typeof fetchUser>>;
// { id: number; name: string }
Awaited unwraps the Promise from the return type of an async function.
Parameters<Function>
Extracts the parameter types of a function as a tuple.
function createUser(name: string, role: 'admin' | 'editor', active: boolean) {
// ...
}
type CreateUserParams = Parameters<typeof createUser>;
// [name: string, role: 'admin' | 'editor', active: boolean]
Useful when a wrapper function needs to accept the same arguments as the wrapped function without duplicating the type definition.
NonNullable<Type>
Removes null and undefined from a type.
type MaybeUser = User | null | undefined;
type DefiniteUser = NonNullable<MaybeUser>;
// User
Often used after a null check to narrow a type for a section of code that has already verified the value is present, or when constructing a type from a union that may have been defined to allow nullability.
Combining Utility Types
Utility types compose naturally. Complex type transformations can be expressed by chaining them:
// A type for updating a user: all User fields optional, but id is excluded
type UserUpdate = Partial<Omit<User, 'id'>>;
// { name?: string; email?: string; role?: 'admin' | 'editor' | 'viewer' }
// A readonly version of only the public fields
type PublicReadonlyUser = Readonly<Pick<User, 'id' | 'name' | 'role'>>;
The readability of composed utility types degrades as more are nested. When a composed type is reused in multiple places, assigning it a named alias keeps the code readable and makes the intent explicit.
Key Takeaways
Partialmakes all fields optional;Requiredmakes all fields mandatory. Both are commonly used at API boundaries where field presence varies.Pickcreates a type from a subset of keys;Omitcreates a type with certain keys removed. Use them to shape types for specific consumers without defining separate interfaces.Readonlyenforces compile-time immutability but does not prevent runtime mutation. It applies to the top-level properties only.Recordmaps a union of keys to a value type and enforces that all keys in the union are present.ReturnTypeandParametersextract types from functions, keeping derived types automatically in sync when the function signature changes.NonNullablestripsnullandundefinedfrom a union type.Utility types compose. Combining two or three produces precize transformations that would otherwize require manual type definitions.
Conclusion
Utility types are the mechanism TypeScript provides for deriving types from existing ones rather than defining everything from scratch. Each one addresses a specific and recurring transformation: making fields optional, excluding sensitive keys, enforcing immutability, mapping a fixed set of keys. Used consistently, they reduce the amount of type maintenance required as a codebase grows and keep related types automatically synchronized with their sources.
Using a utility type in an interesting or non-obvious way? Share it in the comments.




