TypeScript Utility Types 1: Partial, Required, Readonly, Pick, Omit and Record
We use Partial, Required, Readonly, Pick, Omit and Record with realistic examples, combine them in a form model and look at silent pitfalls such as a typo inside Omit.
Version note: The code in this post targets TypeScript 7.0 and Node 26.
Predict First
We have a function that updates a user's details. We don't want to send every field during an update, only the ones that changed:
type User = { id: number; name: string; email: string };
function updateUser(id: number, changes: Partial<User>) {
console.log(id, changes);
}
updateUser(1, { name: "Grace" });
updateUser(1, {});
updateUser(1, { age: 30 });
Which of the three calls compile? If one fails, what does TypeScript object to?
What Is a Utility Type?
Utility types are generic types that ship ready-made in TypeScript's standard library. Just as a function takes a value and returns a new value, these take a type and return a new type. You don't need to import any of them; they're already available in every TypeScript file.
There's no magic inside them. Partial, for example, is written roughly like this in TypeScript's own definition files:
type MyPartial<T> = {
[P in keyof T]?: T[P];
};
This is a mapped type: it produces a property for every key of T, adds a ? to each one, and takes its type from the original with the T[P] we saw in the previous post. We'll cover mapped types in detail in the Advanced series and write every type from this post ourselves. For now we'll use them as ready-made tools.
Here's the model to keep in mind: utility types don't change the original type. When you apply a filter to a photo, the original stays the same and you get a new copy. When you write Partial<User>, User is still the same User; you just have one more type next to it.
Partial and Required
Partial<T> makes every property optional. Back to the opening question: the first two calls compile. { name: "Grace" } is valid because the other fields are no longer required. The empty object {} is valid too, because no field is required. The third call doesn't compile:
error TS2353: Object literal may only specify known properties, and 'age' does not exist in type 'Partial<User>'.
Partial makes fields optional but doesn't accept new ones; the excess property check we saw in the Basics series works here too. Its typical use is a "patch" object:
type User = { id: number; name: string; email: string };
function applyChanges(user: User, changes: Partial<User>): User {
return { ...user, ...changes };
}
const ada: User = { id: 1, name: "Ada", email: "ada@example.com" };
console.log(applyChanges(ada, { email: "ada@lovelace.dev" }));
// { id: 1, name: 'Ada', email: 'ada@lovelace.dev' }
Required<T> is the opposite: it removes every ?. Its most common use is where optional settings get filled in with defaults. Settings coming from outside may be incomplete, but the defaults must be complete:
type Options = {
retries?: number;
timeoutMs?: number;
};
const defaults: Required<Options> = { retries: 3, timeoutMs: 5000 };
function resolveOptions(options: Options): Required<Options> {
return { ...defaults, ...options };
}
console.log(resolveOptions({ retries: 5 })); // { retries: 5, timeoutMs: 5000 }
// Error: Property 'timeoutMs' is missing in type '{ retries: number; }' but required in type 'Required<Options>'.
const incomplete: Required<Options> = { retries: 1 };
When someone adds a new setting to Options, the defaults line fails right away and you can't forget to write its default value.
Readonly
Readonly<T> makes every property readonly. It's a good choice for configuration that shouldn't change while the application runs:
type AppConfig = {
apiUrl: string;
features: { beta: boolean };
};
const config: Readonly<AppConfig> = {
apiUrl: "https://api.example.com",
features: { beta: false },
};
// Error: Cannot assign to 'apiUrl' because it is a read-only property.
config.apiUrl = "https://other.example.com";
config.features.beta = true; // compiles!
const tags: Readonly<string[]> = ["ts", "js"];
// Error: Property 'push' does not exist on type 'readonly string[]'.
tags.push("go");
Look at the line in the middle: Readonly is shallow. It only locks the first-level properties; you can't replace features with another object, but you can change the beta inside it. To lock nested structures completely you need to write your own DeepReadonly; we'll do that with recursive types in the Advanced series.
Also keep in mind that readonly is only a compile-time rule. The object isn't frozen at runtime. If you really want to freeze it, you use Object.freeze, which you know from the JavaScript series; TypeScript already types its return value as Readonly<T>.
Pick and Omit
These two select properties from a type. Pick<T, K> takes only the keys you ask for; Omit<T, K> drops the keys you name and takes the rest. Which one you use depends on which list is shorter:
type User = {
id: number;
name: string;
email: string;
passwordHash: string;
};
type UserPreview = Pick<User, "id" | "name">;
// { id: number; name: string; }
type PublicUser = Omit<User, "passwordHash">;
// { id: number; name: string; email: string; }
function toPublic(user: User): PublicUser {
const { passwordHash, ...rest } = user;
return rest;
}
// Error: Type '"avatar"' does not satisfy the constraint 'keyof User'.
type WithAvatar = Pick<User, "avatar">;
PublicUser is a good example: you don't want the password hash in a response you send out. If you forgot to drop passwordHash in toPublic, the return value would still be assignable to PublicUser, because an object with extra properties is assignable as long as it doesn't trip the excess property check. In other words, Omit describes a type; it doesn't delete the field from the data. You do the deleting yourself with ...rest.
In Pick, the keys are constrained by keyof T, so writing a key that doesn't exist gives you a TS2344 error. Omit has no such constraint; we'll see what that leads to in the pitfalls section.
Record
Record<K, V> produces an object type whose keys are K and whose values are V. We pointed to it when we covered index signatures in the Basics series. Its real strength shows when K is a literal union:
type Role = "admin" | "editor" | "viewer";
const permissions: Record<Role, string[]> = {
admin: ["read", "write", "delete"],
editor: ["read", "write"],
viewer: ["read"],
};
// Error: Property 'viewer' is missing in type '{ admin: string[]; editor: string[]; }' but required in type 'Record<Role, string[]>'.
const partialPermissions: Record<Role, string[]> = {
admin: ["read"],
editor: ["read"],
};
const visits: Record<string, number> = {};
visits["/home"] = 1;
visits["/about"] = 3;
When a new role is added to Role, the permissions object fails right away: you can't compile without writing the new role's permissions. This is the shortest and safest way to define a value for every member of a union. Record<string, number>, on the other hand, is for dictionaries whose keys aren't known in advance, and it means the same as { [key: string]: number }.
Using Them Together: A Form Model
Utility types really pay off when you combine them. Picture a product form: a saved product has an id and a createdAt, but the user doesn't send those when creating a new product. The form's error messages should also be kept field by field:
type Product = {
id: string;
title: string;
price: number;
stock: number;
createdAt: Date;
};
type NewProduct = Omit<Product, "id" | "createdAt">;
type ProductPatch = Partial<NewProduct>;
type ProductErrors = Partial<Record<keyof NewProduct, string>>;
function validate(input: NewProduct): ProductErrors {
const errors: ProductErrors = {};
if (input.title.trim() === "") errors.title = "Title is required";
if (input.price <= 0) errors.price = "Price must be positive";
return errors;
}
const patch: ProductPatch = { stock: 0 };
console.log(validate({ title: "", price: 0, stock: 5 }));
// { title: 'Title is required', price: 'Price must be positive' }
Four types, derived from a single source. When you add a category field to Product, NewProduct, ProductPatch and ProductErrors update on their own. The ProductErrors line may look confusing at first; read it from the inside out: keyof NewProduct is the union of field names, Record gives each field a string message, and Partial says "not every field has to have an error."
Common Pitfalls
type User = { id: number; name: string; email: string };
type WithoutEmail = Omit<User, "emial">; // compiles, email is still there
type Options = { retries?: number };
const defaults: Required<Options> = { retries: 3 };
function resolve(options: Options): Required<Options> {
return { ...defaults, ...options };
}
console.log(resolve({ retries: undefined })); // { retries: undefined }
The first pitfall is a typo in Omit. Omit's second parameter isn't constrained by keyof T, only by "any key". When you write "emial", the compiler doesn't object, nothing is dropped, and WithoutEmail is simply User. Typing key names with your editor's autocomplete is the simplest cure for this mistake.
The second pitfall is sneakier. When you write retries?: number, you're also free to pass undefined to retries explicitly under the default settings. When you merge with spread, that undefined overwrites the default value, yet TypeScript still treats the return type as number. You see undefined in the output. The exactOptionalPropertyTypes setting that closes this gap isn't part of the strict family. The tsconfig generated by tsc --init turns it on, but in a tsconfig you write yourself you have to enable it separately. If it's off, consider filtering out undefined values when merging defaults.
The third is a reminder: Readonly is shallow, and no utility type does anything at runtime. Partial, Omit or Readonly leave no trace in the compiled JavaScript; changing or checking the data is still your code's job.
A Small Experiment
★☆☆ Given type Post = { id: number; title: string; body: string; published: boolean };, write a PostSummary type for a list screen that contains only id and title, first with Pick, then with Omit. Which one came out shorter?
★★☆ Predict whether it compiles: const p: Readonly<Post> = { id: 1, title: "Hi", body: "", published: false }; p.published = true; And what about const q: Partial<Readonly<Post>> = {}; q.title = "x";?
★★★ For type Theme = "light" | "dark" | "contrast";, write a Record that maps each theme name to a color code. Then add "sepia" to Theme and see where the compiler stops you.
The utility types in this post all dealt with the properties of objects. In the next post we move on to the second group: Exclude, Extract and NonNullable, which filter unions, and ReturnType, Parameters, InstanceType and Awaited, which derive types from functions and classes.