TypeScript utility types let you reshape existing types instead of repeating their fields. This guide shows how Partial, Pick, Omit, Record, and Required work in practical user, configuration, and API examples—and where their compile-time transformations stop.
Key Takeaways
Partial<T>makes a type’s properties optional;Required<T>makes them required.Pick<T, K>keeps selected properties, whileOmit<T, K>removes selected properties from the type.Record<K, V>describes an object whose keys come fromKand whose values have typeV.- These utility types are shallow and do not change or validate JavaScript objects at runtime.
- Compile examples with strict checking, and add runtime validation when data comes from outside your program.
Start with Partial: user updates and configuration
Partial<T> is useful when an operation accepts only some of an existing object’s properties. A user update, for example, may allow a caller to change a name or email without supplying the user’s ID and role.
interface User {
id: string;
displayName: string;
email: string;
role: "admin" | "member";
}
type UserPatch = Partial<Pick<User, "displayName" | "email">>;
function updateUser(user: User, patch: UserPatch): User {
return { ...user, ...patch };
}
const currentUser: User = {
id: "u-42",
displayName: "Sam Rivera",
email: "sam@example.com",
role: "member",
};
const updatedUser = updateUser(currentUser, { displayName: "Sam R." });
Here, Pick limits the patch to the fields users are allowed to change, and Partial makes those selected fields optional. Choosing the fields explicitly is safer than accepting Partial<User> when callers must not be able to change id or role.
Use Partial for configuration inputs
A configuration function can accept overrides while returning a complete configuration. Consider a nested retry policy:
interface RetryPolicy {
attempts: number;
delayMs: number;
}
interface ServiceConfig {
endpoint: string;
verbose: boolean;
retry: RetryPolicy;
}
const defaultConfig: ServiceConfig = {
endpoint: "https://api.example.com",
verbose: false,
retry: { attempts: 3, delayMs: 500 },
};
function configure(input: Partial<ServiceConfig>): ServiceConfig {
return {
...defaultConfig,
...input,
retry: {
...defaultConfig.retry,
...input.retry,
},
};
}
const config = configure({ retry: { attempts: 5, delayMs: 250 } });
Partial<ServiceConfig> changes only the top-level properties. If retry is provided, it must still contain both attempts and delayMs. The explicit nested merge preserves unspecified retry defaults when applying an override.
Use Pick and Omit to describe API shapes
Pick<T, K> describes a type containing only selected properties. It is useful for response shapes that intentionally expose a subset of a domain model.
type UserSummary = Pick<User, "id" | "displayName">;
function getUserSummary(user: User): UserSummary {
return { id: user.id, displayName: user.displayName };
}
Omit<T, K> describes a type with selected properties excluded. For example, it can describe a public representation without an email address:
type PublicUser = Omit<User, "email">;
function toPublicUser(user: User): PublicUser {
const { email, ...publicUser } = user;
return publicUser;
}
The Omit type alone does not remove email from a JavaScript object. The function destructures it out at runtime; the type then describes the returned shape. This distinction matters when preparing API responses or handling sensitive fields: a type annotation is not a data-sanitization step.
Create key-to-value maps with Record
Record<K, V> describes an object with keys of type K and values of type V. When K is a finite union, TypeScript checks that the object supplies the expected keys and that their values match.
type RoleLabels = Record<User["role"], string>;
const roleLabels: RoleLabels = {
admin: "Administrator",
member: "Member",
};
function labelRole(role: User["role"]): string {
return roleLabels[role];
}
If another role is added to User["role"], the roleLabels object will need a corresponding entry. For arbitrary string-keyed dictionaries, a type such as Record<string, boolean> describes the value type associated with string keys; it does not mean every possible string key is already present at runtime.
Use Required for resolved options
An input type often makes fields optional because callers can rely on defaults. After defaults have been applied, Required<T> can describe the complete result.
interface SearchOptions {
query?: string;
limit?: number;
}
type ResolvedSearchOptions = Required<SearchOptions>;
function resolveSearchOptions(input: SearchOptions): ResolvedSearchOptions {
return {
query: input.query ?? "",
limit: input.limit ?? 20,
};
}
const options = resolveSearchOptions({ query: "typescript" });
// options has both query and limit.
Required changes optional property modifiers in the type. It does not fill in missing properties, check values, or make untrusted input safe; the function supplies the defaults here. Validate constraints such as a positive limit separately if the application needs them.
Compile the examples under strict mode
Save the examples in a .ts file, with the interface declarations included, then run the TypeScript compiler in strict mode:
npx tsc --strict --noEmit utility-types.ts
This command assumes Node.js and npm are available and that the TypeScript compiler can be run through npx. For a project that already has TypeScript installed, use its local compiler, such as npx tsc --strict --noEmit. The examples are type-level demonstrations and do not require a framework.
Common Mistakes
- Assuming these types are deep.
Partial<ServiceConfig>makesretryoptional, but does not makeretry.attemptsandretry.delayMsoptional. Use a deliberate nested type or merge when nested partial input is required. - Treating a type transformation as runtime behavior.
Omitdoes not delete a property,Requireddoes not supply a value, andRecorddoes not create an object. Write code that performs the runtime work. - Using
Partial<T>for every update request. It may allow changes to fields that the operation should not accept. Select the permitted keys withPickfirst. - Assuming a type prevents all extra properties. TypeScript checks object literals in many contexts, but its structural type system is not a general runtime filter for additional properties.
- Forgetting defaults in shallow merges. Spreading a partial top-level configuration over defaults replaces a whole nested object if that nested property is supplied. Merge nested objects separately when preserving their defaults is required.
Best Practices
- Define a clear base type, then use
Pick,Omit,Partial, orRequiredto express a specific operation’s input or output. - Keep update types narrow. Accept only fields the operation is designed to change.
- Treat values from network requests, files, and other external sources as untrusted. TypeScript utility types do not validate them at runtime.
- Make nested merge behavior explicit; do not assume a shallow utility type or object spread performs a deep merge.
- Run
tsc --strictregularly so examples and application code are checked with strict type rules.
Conclusion
These five utility types make it easier to express related input, output, and configuration shapes without duplicating type definitions. Use them to clarify what a function accepts or returns, and pair compile-time types with explicit runtime logic wherever data must be transformed or validated.
