Skip to content
Theory · Type operators and utility types · lesson 5 of 8

Partial, Required and Readonly

Back to the form generator. Sometimes you need not the form itself but a draft of it: the same fields, but any of them may stay empty for now. That happens when a user changes one field in their profile, say only their age. The program receives not the whole form but only what changed.

Let's try to describe such a change with the type User:

TypeScript
interface User {
    name: string;
    age: number;
    city: string;
}

function updateUser(user: User, changes: User): User {
    return { ...user, ...changes };
}

const ann: User = { name: "Ann", age: 30, city: "Omsk" };
updateUser(ann, { age: 31 }); // ✗
TEXT
error TS2739: Type '{ age: number; }' is missing the following properties from type 'User': name, city

tsc is right: every property of User is required, and the change holds only age. You could write a second interface with a ? on every property. But then every field is written twice again.

The Partial utility type

A utility type is a generic type from TypeScript's standard library that builds a new type from an old one. You don't need to import it: it is available in every file. Partial<User> makes every property of User optional, which gives exactly that draft:

TypeScript
interface User {
    name: string;
    age: number;
    city: string;
}

function updateUser(user: User, changes: Partial<User>): User {
    return { ...user, ...changes };
}

const ann: User = { name: "Ann", age: 30, city: "Omsk" };
const older = updateUser(ann, { age: 31 });
console.log(older.name, older.age); // Ann 31

Let's walk through the example line by line:

  1. Partial<User> is the same type as { name?: string; age?: number; city?: string }.
  2. The object { age: 31 } fits: the other properties may be left out.
  3. { ...user, ...changes } builds a new user: first all the old properties, then the changed ones on top.
  4. The program prints Ann 31: the name stayed the same, and the age was updated.

The other checks are still there. Partial<User> has no email property, so tsc does not let { email: "ann@mail.com" } through. Nor does { age: "31" } pass: age still holds a number.

Here the draft picture stops working. A draft filled in to the end can be handed in as the form, while a value of type Partial<User> does not fit User even if it has every field. tsc sees the type, and in that type any property may be missing.

How Partial is built

Partial is not a special compiler mechanism. It is made of keyof and indexed access, which you already know, and you can write the same type yourself:

TypeScript
interface User {
    name: string;
    age: number;
    city: string;
}

type Draft<T> = { [K in keyof T]?: T[K] };

const change: Draft<User> = { city: "Tomsk" };
console.log(change.city); // Tomsk

The notation { [K in keyof T]?: T[K] } reads like this:

  • [K in keyof T] means "for each name K in keyof T": the property repeats for every name;
  • ? after the brackets makes each such property optional;
  • T[K] keeps the property's type the same as in T.

The standard library writes Partial almost the same way, except that its parameter is called P. Required and Readonly follow the same pattern: they also go over keyof T and change one mark on every property.

Required and Readonly

Required

Required<T> does the opposite: it removes the ? from every property. It comes in handy for settings with default values. Say the settings are described as interface Settings { theme?: string; fontSize?: number }: a user may set any of them. But the default values must have everything filled in:

  • the line const defaults: Required<Settings> = { theme: "dark", fontSize: 14 }; compiles;
  • if you forget fontSize, tsc names the missing property.
TEXT
error TS2741: Property 'fontSize' is missing in type '{ theme: string; }' but required in type 'Required<Settings>'.

Readonly

Readonly<T> marks every property readonly, as if you had written that word on each of them. After const ann: Readonly<User> = { name: "Ann", age: 30, city: "Omsk" }; the line ann.age = 31 does not compile:

TEXT
error TS2540: Cannot assign to 'age' because it is a read-only property.

Like readonly itself, this protection is checked only by tsc. Readonly<User> does not freeze the object: it vanishes from the JavaScript along with the other types, and at run time the property can be changed.

Interviewers often ask what Partial, Required and Readonly do. The answer: each goes over every property of T and adds or removes one mark on them, ? or readonly.

Common mistake

You expect Partial to make the properties of a nested object optional too:

TypeScript
interface Order {
    id: number;
    address: { city: string; street: string };
}

const fix: Partial<Order> = { address: { city: "Omsk" } }; // ✗
TEXT
error TS2741: Property 'street' is missing in type '{ city: string; }' but required in type '{ city: string; street: string; }'.

All three utilities change only the top-level properties. In Partial<Order> you may leave out address, but if it is there, it is a full address with every field. In the same way, Readonly<Order> forbids replacing address as a whole but allows changing address.city. To change the city, pass the whole address: { address: { city: "Omsk", street: "Lenina" } }.

Key points

  • Partial<T> makes every property of T optional, Required<T> makes them required, Readonly<T> makes them read-only.
  • All three go over keyof T, as { [K in keyof T]?: T[K] } does for Partial, and change one mark on every property.
  • They change only the top level, and the properties of a nested object stay as they were.
  • They are types: tsc checks and erases them, and Readonly does not freeze the object at run time.