On this page
Utility types let you express relationships between types instead of copying property lists into several interfaces. That saves maintenance only when the relationship is real. A public profile and a database row may share fields today without being the same contract tomorrow. Use utilities to capture intent, not merely to reduce typing.
Begin with a trustworthy domain model
Before choosing a utility, identify which type owns the underlying facts. A user record might include an identifier, display name, email address, account role, and creation timestamp. An update operation should not automatically accept every field merely because those properties exist on the record.
Paste a sanitized payload into the JSON to TypeScript tool to generate an initial structural model. Then review optionality, nullable values, and string unions against the actual contract. Generated types describe a sample; they do not establish which properties callers may edit or which data is safe to expose.
Utility types operate at compile time. They neither validate incoming JSON nor delete object properties at runtime. The TypeScript utility reference is the authoritative catalog; this guide focuses on combinations that keep everyday application contracts understandable.
Use Partial and Required for property presence
Partial makes every top-level property optional. Required removes optional markers. Both are shallow transformations: nested objects keep their original definitions unless you transform them separately. This is often exactly what a patch operation needs, provided you first narrow the editable fields.
interface User {
id: string;
displayName: string;
email: string;
role: "member" | "admin";
preferences: { theme: "light" | "dark"; locale: string };
}
type EditableUser = Pick<User, "displayName" | "preferences">;
type UserPatch = Partial<EditableUser>;
const patch: UserPatch = {
preferences: { theme: "dark", locale: "en-GB" }
};Here preferences may be omitted, but if supplied it still requires both theme and locale. A deep-partial helper would change that rule and would need explicit handling for arrays, functions, and special objects. Do not add recursive machinery until your update protocol actually supports recursive merging.
An absent property is not always equivalent to a property explicitly set to undefined. Enabling exactOptionalPropertyTypes makes that difference more visible in assignments. JSON also omits undefined object properties during serialization, so a TypeScript patch object and its transmitted representation can differ.
Required does not fill missing values. It only describes a type where properties must be present. To normalize configuration, write a function that supplies defaults and returns the completed type. A type assertion cannot perform that normalization, and it can conceal the very missing values the utility was supposed to prevent.
Choose Pick for allowlists and Omit for subtraction
Pick selects named properties. Omit removes named properties. They look symmetrical, but their maintenance behavior differs: a new source property automatically enters an Omit-derived type unless explicitly excluded. It does not enter a Pick-derived type without an intentional edit.
type PublicProfile = Pick<User, "id" | "displayName">;
type UserWithoutRole = Omit<User, "role">;
function toPublicProfile(user: User): PublicProfile {
return { id: user.id, displayName: user.displayName };
}Use a positive allowlist for externally visible shapes. If the domain model later gains a secret recovery token, a public type defined by omitting only email could accidentally grow to include it. An explicit serializer is still required because narrowing an object's static type does not remove runtime properties.
Structural typing also means a variable with extra properties can often be assigned to a narrower type. Excess-property checks on fresh object literals are useful diagnostics, not an exact-object security boundary. The object types handbook explains these assignability rules.
For reusable presentation components, Pick can state a small dependency surface without coupling the component to unrelated fields. For an independently versioned API, an explicit interface may be better: a change to an internal model should not silently redefine a public promise.
Model dictionaries with Record and Readonly
Record maps a key type to a value type. With a finite union of keys, it checks that all expected entries exist. That is useful for labels, handlers, and configuration maps where missing a case should fail compilation rather than appear as undefined later.
type Status = "draft" | "published" | "archived";
const labels = {
draft: "Draft",
published: "Published",
archived: "Archived"
} satisfies Record<Status, string>;
type SparseUsers = Partial<Record<string, User>>;
type UserView = Readonly<Pick<User, "id" | "displayName">>;Record with arbitrary string keys does not prove that a runtime lookup exists. For sparse data, model absence explicitly or enable noUncheckedIndexedAccess to surface possible undefined results. Map can be clearer when entries are frequently inserted and deleted or keys are not naturally strings.
Readonly blocks writes through the typed reference, but does not freeze the object. It is shallow, too. Another mutable alias may still change the same value. If immutability is a runtime requirement, adopt an appropriate runtime strategy rather than assuming a modifier has changed JavaScript behavior.
Keep map keys aligned with domain unions, not with display labels that translators may change. The design tokens guide offers a related example of separating stable identifiers from presentation values. A status key is a contract; its visible label is content.
Derive unions and function contracts carefully
Exclude removes assignable members from a union; Extract retains them. NonNullable removes null and undefined. Unlike Omit, these helpers work on union membership rather than object property lists. For example, Exclude of a status union and archived represents only the remaining statuses.
ReturnType derives a function's return type, Parameters produces its parameter tuple, and Awaited unwraps promise-like results. Together they help test adapters stay aligned with the implementation they wrap. Prefer Awaited of ReturnType for an asynchronous loader's resolved result rather than manually repeating its payload type.
There are limits. Overloaded functions generally expose the final signature to these inference helpers, and generic functions may yield broader results than expected. If consumers need a stable named contract, export that type explicitly rather than forcing them to reverse-engineer a complex implementation.
Also be cautious with object unions: built-in Pick and Omit do not automatically preserve each variant the way many developers expect. A discriminated union deserves a focused compile-time test before a utility is applied across it. Sometimes writing the two variants explicitly communicates the contract more clearly than a sophisticated conditional helper.
Use the development hub to connect these patterns with surrounding application concerns, and consult conditional types before building custom distributive utilities. Readable contracts usually beat clever aliases. If a reviewer cannot explain an alias without mentally expanding four transformations, introduce a descriptive intermediate type or write the shape directly.
Key takeaways
- Use utilities to preserve meaningful relationships, not incidental similarity.
- Partial, Required, and Readonly are shallow compile-time transformations.
- Prefer Pick-based allowlists for public views and editable fields.
- Model missing dictionary entries instead of assuming every key exists.
- Keep runtime behavior separate from static assignability and type inference.
Reach designers & developers at work
Put your product next to the tools people use every day. One sponsor per placement, no tracking pixels.
Become a sponsor