Difficulty: Advanced
TypeScript provides several built-in utility types that transform existing types into new ones. `Partial<T>`, `Required<T>`, and `Readonly<T>` are the most fundamental of these. They modify the property modifiers of a type - making properties optional, required, or read-only respectively. Understanding these utilities is essential because they appear everywhere in real-world TypeScript code.
`Partial<T>` makes every property of `T` optional by adding the `?` modifier. This is commonly used for update functions where you only want to change some properties of an object. Instead of requiring the caller to pass the entire object, `Partial<T>` lets them pass only the fields they want to change. Under the hood, it is defined as `{ [K in keyof T]?: T[K] }`.
`Required<T>` is the opposite of `Partial` - it removes the `?` modifier from all properties, making them mandatory. This is useful when you have a type with optional properties but need a complete version for certain operations like database insertion or serialization. Its definition uses the `-?` modifier: `{ [K in keyof T]-?: T[K] }`.
`Readonly<T>` adds the `readonly` modifier to every property, preventing reassignment after construction. This is valuable for enforcing immutability patterns, especially when passing objects to functions that should not modify them. Note that `Readonly<T>` is shallow - it only makes the top-level properties readonly. Nested objects can still be mutated unless they are also wrapped in `Readonly`.
interface User {
name: string;
email: string;
age: number;
}
function updateUser(user: User, updates: Partial<User>): User {
return { ...user, ...updates };
}
const user: User = { name: "Alice", email: "alice@test.com", age: 30 };
const updated = updateUser(user, { age: 31 });
console.log(updated.name);
console.log(updated.age);
const updated2 = updateUser(user, { name: "Alicia", email: "alicia@test.com" });
console.log(updated2.name);
console.log(updated2.email);
Partial<User> makes all User properties optional, so the caller can pass any subset of fields. The spread operator merges the updates with the original.
interface FormData {
name?: string;
email?: string;
phone?: string;
}
function validateForm(data: FormData): Required<FormData> | null {
if (!data.name || !data.email || !data.phone) {
return null;
}
return data as Required<FormData>;
}
const valid = validateForm({ name: "Bob", email: "bob@test.com", phone: "555-1234" });
if (valid) {
console.log(`${valid.name} - ${valid.email} - ${valid.phone}`);
}
const invalid = validateForm({ name: "Bob" });
console.log(invalid);
Required<FormData> ensures all properties are present. The validation function checks this at runtime and returns the stricter type or null.
interface Config {
apiUrl: string;
timeout: number;
retries: number;
}
function createConfig(overrides: Partial<Config> = {}): Readonly<Config> {
return Object.freeze({
apiUrl: "https://api.example.com",
timeout: 5000,
retries: 3,
...overrides
});
}
const config = createConfig({ timeout: 10000 });
console.log(config.apiUrl);
console.log(config.timeout);
console.log(config.retries);
// config.timeout = 3000; // Error: Cannot assign to 'timeout' because it is a read-only property
Readonly<Config> prevents any reassignment to the config properties. Object.freeze provides the runtime immutability to match the type-level guarantee.
Partial<T>, Required<T>, Readonly<T>, mapped type modifiers, immutability