Object Types & Optional Props

Difficulty: Beginner

Question

How do you define object types in TypeScript? Explain optional properties, readonly, and excess property checking.

Answer

TypeScript uses structural typing (duck typing) for objects - if an object has the required shape, it is compatible with the type, regardless of its declared name.

Key features: - Optional properties (?): can be present or absent - Readonly properties: cannot be reassigned after creation - Excess property checking: TypeScript catches extra properties in object literals - Structural compatibility: types are compatible based on shape, not name

This system provides flexibility while maintaining safety. Understanding structural typing is essential because it differs from nominal typing used in Java or C#.

Code examples

Object Types and Optional Properties

// Inline object type
function createUser(config: {
  name: string;
  email: string;
  age?: number;    // optional
  bio?: string;    // optional
}): User {
  return { id: Date.now(), ...config, age: config.age ?? 0, bio: config.bio ?? '' };
}

// Interface with optional and readonly
interface Config {
  readonly apiUrl: string;     // cannot be changed
  readonly apiKey: string;
  timeout?: number;            // optional
  retries?: number;
}

const config: Config = {
  apiUrl: 'https://api.example.com',
  apiKey: 'secret-key',
  timeout: 5000,
};
// config.apiUrl = 'new-url'; // Error: Cannot assign to 'apiUrl' because it is read-only

// Readonly utility type - makes ALL properties readonly
type FrozenConfig = Readonly<Config>;

Optional properties are marked with ?. Readonly prevents reassignment. Use Readonly<T> to make all properties readonly at once.

Structural Typing (Duck Typing)

interface Point {
  x: number;
  y: number;
}

// This works because the object has the right shape
const point: Point = { x: 10, y: 20 };

// Structural compatibility - no 'implements' needed
interface Named {
  name: string;
}
function greet(obj: Named): string {
  return `Hello, ${obj.name}!`;
}

// Any object with a 'name' property works
greet({ name: 'Alice' });
greet({ name: 'Bob', age: 30 }); // Error! Excess property check

// But assigning to a variable first bypasses the check
const person = { name: 'Bob', age: 30 };
greet(person); // OK - no excess property check on variables

// This is structural typing: shape matters, not name
class Dog {
  constructor(public name: string) {}
}
greet(new Dog('Rex')); // OK - Dog has 'name' property

Structural typing means any value with the required properties is compatible. Excess property checking only applies to object literals directly assigned.

Nested Objects and Utility Patterns

// Nested object types
interface Company {
  name: string;
  address: {
    street: string;
    city: string;
    country: string;
    zipCode?: string;
  };
  employees: {
    id: number;
    name: string;
    role: string;
  }[];
}

// Partial for updates (all props optional)
type CompanyUpdate = Partial<Company>;

// Required - make all props required
type StrictCompany = Required<Company>;

// Deep readonly pattern
type DeepReadonly<T> = {
  readonly [K in keyof T]: T[K] extends object ? DeepReadonly<T[K]> : T[K];
};

const company: DeepReadonly<Company> = {
  name: 'Acme',
  address: { street: '123 Main', city: 'NYC', country: 'US' },
  employees: [{ id: 1, name: 'Alice', role: 'dev' }],
};
// company.address.city = 'LA'; // Error: Cannot assign to 'city'

Combine interfaces with utility types for common patterns like partial updates or immutable data. DeepReadonly is a common custom utility type.

Key points

Concepts covered

Object Types, Optional Properties, Readonly, Excess Property Checking, Structural Typing