Partial, Required & Readonly
Partial, Required, and Readonly are the three foundational utility types for reshaping the modifiers on an object’s properties. Each takes an object type and returns a new type with every property flipped to optional, mandatory, or immutable respectively. They are implemented as simple homomorphic mapped types, so they preserve the original keys while toggling a single modifier. You will reach for them constantly when building update payloads, normalizing partial input, or locking down configuration objects.
Partial: every property optional
Partial<T> produces a copy of T where each property is marked optional (?). It is the workhorse for “patch” or “update” objects, where a caller supplies only the fields they want to change.
interface User {
id: number;
name: string;
email: string;
}
function updateUser(id: number, changes: Partial<User>): void {
// changes can be {}, { name } , { email, name }, etc.
}
updateUser(1, { name: "Ada" }); // ✅ ok
updateUser(1, {}); // ✅ ok
updateUser(1, { role: "admin" }); // ❌ Error: 'role' does not exist on Partial<User>
Every key of User becomes optional, but no new keys are allowed — the shape is still constrained to User’s keys. This is exactly what you want for a merge-style update.
Required: every property mandatory
Required<T> is the inverse of Partial: it strips the optional modifier from every property, making them all required. It also removes undefined from the property type when exactOptionalPropertyTypes is off.
interface Config {
host?: string;
port?: number;
secure?: boolean;
}
function startServer(config: Required<Config>): void {
// host, port, and secure are all guaranteed present here
console.log(`${config.secure ? "https" : "http"}://${config.host}:${config.port}`);
}
// ❌ Error: missing 'port' and 'secure'
startServer({ host: "localhost" });
This is useful after you merge user-supplied options with defaults: the resolved object satisfies Required<Config>, signalling downstream code that nothing can be missing.
Readonly: every property immutable
Readonly<T> marks each property with the readonly modifier, so reassignment is a compile error. Note that this is shallow — nested objects remain mutable.
interface Point {
x: number;
y: number;
}
const origin: Readonly<Point> = { x: 0, y: 0 };
origin.x = 10; // ❌ Error: Cannot assign to 'x' because it is a read-only property
Readonlyis a compile-time guarantee only. It disappears after compilation and does not callObject.freeze. For runtime immutability, useObject.freeze(andas constfor literal values).
How they are built
All three are tiny homomorphic mapped types. The ? and readonly modifiers can be added with + or removed with -:
type MyPartial<T> = { [K in keyof T]?: T[K] };
type MyRequired<T> = { [K in keyof T]-?: T[K] }; // -? strips optional
type MyReadonly<T> = { readonly [K in keyof T]: T[K] };
Because they iterate over keyof T, they automatically track changes to the source type. Understanding these definitions makes it easy to compose your own variants, such as a deep version.
Comparison at a glance
| Utility | Modifier change | Typical use case |
|---|---|---|
Partial<T> | adds ? to all props | update / patch payloads |
Required<T> | removes ? from all props | resolved config after defaults |
Readonly<T> | adds readonly to all props | immutable constants, props |
Building a deep variant
The built-ins are shallow. For a recursive version, combine a mapped type with a conditional check — see recursive types:
type DeepReadonly<T> = {
readonly [K in keyof T]: T[K] extends object ? DeepReadonly<T[K]> : T[K];
};
const settings: DeepReadonly<{ db: { url: string } }> = { db: { url: "..." } };
settings.db.url = "x"; // ❌ Error: read-only
Best Practices
- Use
Partial<T>for update functions, but validate at runtime that at least one field is present if your API requires it. - Reach for
Required<T>to express “all defaults have been applied” rather than annotating each field manually. - Remember
Readonly<T>is shallow and compile-time only — pair it withObject.freezeoras constwhen you need runtime immutability. - Prefer
readonlyarrays (ReadonlyArray<T>orreadonly T[]) for parameters you never mutate. - Compose utilities (
Readonly<Partial<T>>) instead of writing bespoke mapped types when the built-ins suffice. - Write a
DeepPartial/DeepReadonlyhelper once and reuse it rather than nesting the shallow versions. - Keep source interfaces minimal; these utilities derive everything else, so you avoid duplicated shapes.