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:
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 }); // ✗error TS2739: Type '{ age: number; }' is missing the following properties from type 'User': name, citytsc 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:
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 31Let's walk through the example line by line:
Partial<User>is the same type as{ name?: string; age?: number; city?: string }.- The object
{ age: 31 }fits: the other properties may be left out. { ...user, ...changes }builds a new user: first all the old properties, then the changed ones on top.- 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:
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); // TomskThe notation { [K in keyof T]?: T[K] } reads like this:
[K in keyof T]means "for each nameKinkeyof 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 inT.
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.
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:
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:
interface Order {
id: number;
address: { city: string; street: string };
}
const fix: Partial<Order> = { address: { city: "Omsk" } }; // ✗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 ofToptional,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 forPartial, 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
Readonlydoes not freeze the object at run time.