#How utility types work
A utility type is a generic type alias that ships with TypeScript. You hand it a type, and it hands you back a new type: Partial<User> is "User, but every property optional"; ReturnType<typeof f> is "whatever f returns". There are about twenty of them, they're global (no import), and they all live in the compiler's standard library file lib.es5.d.ts.
The mental model that makes them click: utility types are functions on types. Partial<User> reads exactly like a function call partial(User) — the angle brackets are the parentheses, T is the parameter, and the right-hand side of the type declaration is the function body. The checker "calls" it at compile time, substitutes User for T, evaluates the body, and the result is a brand-new type. Like a pure function, it never changes its input: after type Draft = Partial<User>, User is exactly as strict as before. And like every other type, the result is erased — none of this exists in the emitted JavaScript.
Almost all of them are built from three pieces of type-level machinery you'll meet properly in Mapped & Conditional Types: mapped types that loop over keys (Partial, Required, Readonly, Pick, Record), conditional types that branch, distribute over unions and pattern-match with infer (Exclude, Extract, ReturnType, Parameters, Awaited and friends), and a plain intersection (NonNullable). A few are intrinsic — implemented inside the compiler because they can't be written in TypeScript (Uppercase and the other string types, NoInfer) — and one, ThisType, is just a marker the compiler recognises by name.
interface User {
id: number;
name: string;
email: string;
}
type UserPatch = Partial<User>;
// type UserPatch = { id?: number; name?: string; email?: string }
const patch: UserPatch = { email: "new@example.com" };
const full: User = { email: "new@example.com" }; // ❌ TS2739: Type '{ email: string; }' is missing the following properties from type 'User': id, namePartial<User>is a "call": the checker findstype Partial<T> = { [P in keyof T]?: T[P] }inlib.es5.d.tsand binds the parameterTtoUser.- It evaluates the body for that argument: it walks the keys
"id" | "name" | "email"and produces one optional property per key. The result is a new anonymous object type, which the aliasUserPatchnames. patchonly hasemail, which is fine — every property ofUserPatchis optional.fullis annotated with the originalUser, and the compiler still demandsidandname(TS2739). This is the "pure function" point: makingUserPatchdidn't loosenUserat all.- In the emitted JavaScript both lines are just
const patch = { email: … }— utility types are a compile-time calculation only. They can describe a value; they can't change one.
Because they're functions, they compose, and they evaluate inside-out like nested calls:
interface User {
id: number;
name: string;
email: string;
passwordHash: string;
}
type PublicProfile = Readonly<Pick<User, "name" | "email">>;
// type PublicProfile = { readonly name: string; readonly email: string }
const profile: PublicProfile = { name: "Rohit", email: "rohit@example.com" };
profile.name = "R"; // ❌ TS2540: Cannot assign to 'name' because it is a read-only property.- The innermost call runs first:
Pick<User, "name" | "email">produces{ name: string; email: string }— two of the four properties. - That result becomes the argument to
Readonly<…>, which walks its keys (name,email) and addsreadonlyto each. passwordHashandidnever appear in the output, soPublicProfileis safe to send to a client — at the type level (whether the object still carries them is a separate question; see Pick).- Assigning to
profile.namefails with TS2540, because the outerReadonlymarked it read-only.
Where they live, and how to read them: in your editor, Go to Definition on Partial jumps straight into lib.es5.d.ts. In TypeScript 5.x that file is in node_modules/typescript/lib/. In TypeScript 7 (the native Go compiler) the lib files ship inside the platform package, e.g. node_modules/@typescript/typescript-darwin-arm64/lib/lib.es5.d.ts. Every definition quoted in this chapter is copied from that file (TypeScript 7.0). Reading them is the fastest way to answer "what exactly does this do with optional properties / unions / generics?".
The full set, grouped by how they're built:
| Family | Utility types | Built with |
|---|---|---|
| Change modifiers | Partial, Required, Readonly | homomorphic mapped type over keyof T |
| Choose keys | Pick, Omit | mapped type over a key union (Omit = Pick + Exclude) |
| Build objects | Record | mapped type over any key union |
| Filter unions | Exclude, Extract, NonNullable | distributive conditional type / intersection with {} |
| Take functions apart | ReturnType, Parameters, ConstructorParameters, InstanceType | conditional type + infer |
| Promises | Awaited | recursive conditional type + infer |
| Strings | Uppercase, Lowercase, Capitalize, Uncapitalize | compiler intrinsic |
| Markers | ThisType, NoInfer (see type argument inference) | recognised by name / intrinsic |
When to reach for one: whenever a type is a variation of another type you already have. Deriving UserPatch from User means that when someone adds a field to User, every derived type picks it up automatically, instead of a hand-copied interface silently drifting out of date. When not to: if the derived type has genuinely different meaning (an API contract you want frozen even if the database model changes), write it out explicitly — coupling it to User would be the bug.