Difficulty: Beginner
How do you define object types in TypeScript? Explain optional properties, readonly, and excess property checking.
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#.
// 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.
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 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.
Object Types, Optional Properties, Readonly, Excess Property Checking, Structural Typing