Partial, Required & Readonly

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`.

Code examples

Partial for Update Functions

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.

Required for Strict Validation

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.

Readonly for Immutability

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.

Key points

Concepts covered

Partial<T>, Required<T>, Readonly<T>, mapped type modifiers, immutability