Lesson 3.6: Optional and Readonly Properties
Duration: 50 minutes
Learning Objectives
By the end of this lesson, you will be able to:
- Mark properties as optional using the
?modifier - Protect properties from modification with
readonly - Handle optional properties safely in your code
- Design interfaces that accurately model real-world data
Optional Properties
Not all objects have all properties filled in. Optional properties handle this reality.
The Problem Without Optional Properties
interface User {
id: string;
name: string;
email: string;
phone: string; // What if user didn't provide a phone?
}
const alice: User = {
id: 'user_1',
name: 'Alice',
email: 'alice@example.com',
// Error! Property 'phone' is missing
};
The Solution: Optional Properties
Add ? after the property name to make it optional:
interface User {
id: string;
name: string;
email: string;
phone?: string; // Optional - may or may not exist
}
const alice: User = {
id: 'user_1',
name: 'Alice',
email: 'alice@example.com',
// No error - phone is optional
};
const bob: User = {
id: 'user_2',
name: 'Bob',
email: 'bob@example.com',
phone: '555-0123', // Also valid - phone is provided
};
Working with Optional Properties
Checking for Existence
Optional properties might be undefined, so check before using:
interface User {
name: string;
nickname?: string;
}
function greet(user: User) {
// Without check - TypeScript warns about possible undefined
console.log(user.nickname.toUpperCase()); // Error! Object is possibly 'undefined'
// With check - safe to use
if (user.nickname) {
console.log(`Hey, ${user.nickname.toUpperCase()}!`);
} else {
console.log(`Hello, ${user.name}!`);
}
}
Using Default Values
Provide defaults for missing properties:
interface Settings {
theme?: string;
fontSize?: number;
notifications?: boolean;
}
function applySettings(settings: Settings) {
const theme = settings.theme ?? 'light';
const fontSize = settings.fontSize ?? 14;
const notifications = settings.notifications ?? true;
console.log(`Theme: ${theme}, Size: ${fontSize}, Notifications: ${notifications}`);
}
applySettings({}); // Uses all defaults
applySettings({ theme: 'dark' }); // Uses dark theme, defaults for rest
applySettings({ theme: 'dark', fontSize: 18, notifications: false }); // All specified
The ?? operator (nullish coalescing) returns the right side if the left is null or undefined.
Optional Chaining
Access nested optional properties safely:
interface Company {
name: string;
address?: {
street: string;
city: string;
};
}
function getCity(company: Company): string {
// Without optional chaining - verbose
if (company.address) {
return company.address.city;
}
return 'Unknown';
// With optional chaining - concise
return company.address?.city ?? 'Unknown';
}
Optional Function Parameters
Functions can have optional parameters too:
function createUser(name: string, email: string, age?: number) {
const user = {
name,
email,
age: age ?? 0, // Default to 0 if not provided
};
return user;
}
createUser('Alice', 'alice@example.com'); // age is undefined
createUser('Bob', 'bob@example.com', 28); // age is 28
Rule: Optional parameters must come after required parameters.
// Error! Required parameter cannot follow optional
function badFunction(optional?: string, required: string) {}
// Correct
function goodFunction(required: string, optional?: string) {}
Readonly Properties
Some properties should never change after creation. Use readonly to enforce this.
Basic Readonly
interface User {
readonly id: string;
name: string;
email: string;
}
const user: User = {
id: 'user_123',
name: 'Alice',
email: 'alice@example.com',
};
user.name = 'Alicia'; // OK - name can change
user.id = 'user_456'; // Error! Cannot assign to 'id' because it is a read-only property
Common Use Cases for Readonly
IDs and Timestamps:
interface Entity {
readonly id: string;
readonly createdAt: Date;
updatedAt: Date; // Can be modified
}
Configuration Objects:
interface AppConfig {
readonly apiUrl: string;
readonly maxRetries: number;
readonly timeout: number;
}
const config: AppConfig = {
apiUrl: 'https://api.example.com',
maxRetries: 3,
timeout: 5000,
};
config.apiUrl = 'https://other.com'; // Error! Read-only
Immutable Data:
interface Point {
readonly x: number;
readonly y: number;
}
function movePoint(point: Point, dx: number, dy: number): Point {
// Cannot modify the original point
// point.x += dx; // Error!
// Return a new point instead
return {
x: point.x + dx,
y: point.y + dy,
};
}
Readonly Arrays
Use ReadonlyArray<T> or readonly T[] for arrays that should not be modified:
const numbers: readonly number[] = [1, 2, 3, 4, 5];
numbers.push(6); // Error! Property 'push' does not exist on type 'readonly number[]'
numbers[0] = 10; // Error! Index signature in type 'readonly number[]' only permits reading
numbers.pop(); // Error! Property 'pop' does not exist
// Reading is fine
console.log(numbers[0]); // OK
console.log(numbers.length); // OK
const doubled = numbers.map((n) => n * 2); // OK - creates new array
Function Parameters with Readonly Arrays
function sum(numbers: readonly number[]): number {
// Cannot accidentally modify the input
// numbers.push(0); // Error!
return numbers.reduce((a, b) => a + b, 0);
}
const values = [1, 2, 3, 4, 5];
console.log(sum(values)); // 15
// values array is unchanged
Combining Optional and Readonly
Properties can be both optional and readonly:
interface User {
readonly id: string;
name: string;
email: string;
readonly createdAt: Date;
phone?: string;
readonly deletedAt?: Date; // Optional AND readonly
}
const user: User = {
id: 'user_1',
name: 'Alice',
email: 'alice@example.com',
createdAt: new Date(),
// phone and deletedAt are optional
};
// Can add phone later (it's optional but not readonly)
user.phone = '555-0123'; // OK
// Cannot change deletedAt once set (readonly)
// user.deletedAt = new Date(); // Error! Cannot assign - it's readonly
The Readonly Utility Type
TypeScript provides a utility type to make all properties readonly:
interface User {
id: string;
name: string;
email: string;
}
type ReadonlyUser = Readonly<User>;
// Equivalent to:
// interface ReadonlyUser {
// readonly id: string;
// readonly name: string;
// readonly email: string;
// }
const user: ReadonlyUser = {
id: 'user_1',
name: 'Alice',
email: 'alice@example.com',
};
user.name = 'Bob'; // Error! All properties are readonly
The Partial Utility Type
Make all properties optional with Partial<T>:
interface User {
id: string;
name: string;
email: string;
age: number;
}
type PartialUser = Partial<User>;
// Equivalent to:
// interface PartialUser {
// id?: string;
// name?: string;
// email?: string;
// age?: number;
// }
// Useful for update functions
function updateUser(id: string, updates: Partial<User>) {
// Can update any subset of properties
}
updateUser('user_1', { name: 'Alice' }); // OK - only updating name
updateUser('user_1', { email: 'new@example.com', age: 29 }); // OK - updating multiple
Practical Examples
Example 1: User Profile System
interface UserProfile {
readonly userId: string;
readonly createdAt: Date;
username: string;
displayName: string;
bio?: string;
website?: string;
location?: string;
avatar?: string;
}
function createProfile(userId: string, username: string): UserProfile {
return {
userId,
createdAt: new Date(),
username,
displayName: username,
};
}
function updateProfile(
profile: UserProfile,
updates: Partial<Omit<UserProfile, 'userId' | 'createdAt'>>
): UserProfile {
return {
...profile,
...updates,
};
}
let profile = createProfile('user_1', 'alice');
profile = updateProfile(profile, {
displayName: 'Alice Smith',
bio: 'TypeScript enthusiast',
});
Example 2: Form State
interface FormField {
value: string;
error?: string;
touched?: boolean;
readonly name: string;
}
interface LoginForm {
readonly formId: string;
email: FormField;
password: FormField;
rememberMe?: boolean;
}
const loginForm: LoginForm = {
formId: 'login_form',
email: {
name: 'email',
value: '',
},
password: {
name: 'password',
value: '',
},
};
// Update field values
loginForm.email.value = 'alice@example.com';
loginForm.email.touched = true;
// Cannot change field names
// loginForm.email.name = "user_email"; // Error!
Example 3: Immutable Configuration
interface DatabaseConfig {
readonly host: string;
readonly port: number;
readonly database: string;
readonly ssl?: boolean;
}
interface ServerConfig {
readonly port: number;
readonly hostname: string;
}
interface AppConfig {
readonly database: DatabaseConfig;
readonly server: ServerConfig;
debug?: boolean; // Can be toggled
}
const config: AppConfig = {
database: {
host: 'localhost',
port: 5432,
database: 'myapp',
ssl: true,
},
server: {
port: 3000,
hostname: 'localhost',
},
debug: true,
};
// Can toggle debug mode
config.debug = false; // OK
// Cannot change server config
// config.server.port = 8080; // Error!
Exercises
Exercise 1: Optional Profile Fields
Create an interface for a social media profile where only username and email are required:
Solution
interface SocialProfile {
username: string;
email: string;
displayName?: string;
bio?: string;
avatarUrl?: string;
website?: string;
followers?: number;
following?: number;
}
const minimalProfile: SocialProfile = {
username: 'alice',
email: 'alice@example.com',
};
const fullProfile: SocialProfile = {
username: 'bob',
email: 'bob@example.com',
displayName: 'Bob Smith',
bio: 'Developer',
avatarUrl: 'https://example.com/avatar.jpg',
website: 'https://bob.dev',
followers: 1000,
following: 500,
};
Exercise 2: Readonly Configuration
Create a readonly configuration for an API client:
Solution
interface ApiClientConfig {
readonly baseUrl: string;
readonly apiKey: string;
readonly timeout: number;
readonly retries: number;
readonly headers: Readonly<Record<string, string>>;
}
const apiConfig: ApiClientConfig = {
baseUrl: 'https://api.example.com',
apiKey: 'secret_key_123',
timeout: 5000,
retries: 3,
headers: {
'Content-Type': 'application/json',
Accept: 'application/json',
},
};
// Cannot modify any config
// apiConfig.baseUrl = "https://other.com"; // Error!
// apiConfig.headers["Authorization"] = "Bearer token"; // Error!
Exercise 3: Update Function with Partial
Create a todo item interface and an update function:
Solution
interface Todo {
readonly id: string;
title: string;
description?: string;
completed: boolean;
priority?: 'low' | 'medium' | 'high';
readonly createdAt: Date;
dueDate?: Date;
}
function createTodo(title: string): Todo {
return {
id: crypto.randomUUID(),
title,
completed: false,
createdAt: new Date(),
};
}
function updateTodo(todo: Todo, updates: Partial<Omit<Todo, 'id' | 'createdAt'>>): Todo {
return {
...todo,
...updates,
};
}
let myTodo = createTodo('Learn TypeScript');
myTodo = updateTodo(myTodo, {
completed: true,
priority: 'high',
});
console.log(myTodo);
Exercise 4: Safe Property Access
Write a function that safely accesses nested optional properties:
Solution
interface Company {
name: string;
headquarters?: {
address?: {
street?: string;
city?: string;
country?: string;
};
};
}
function getHeadquartersCity(company: Company): string {
return company.headquarters?.address?.city ?? 'Unknown';
}
function getFullAddress(company: Company): string {
const addr = company.headquarters?.address;
if (!addr) {
return 'No address available';
}
const parts = [addr.street, addr.city, addr.country].filter(Boolean);
return parts.length > 0 ? parts.join(', ') : 'Incomplete address';
}
const techCorp: Company = {
name: 'TechCorp',
headquarters: {
address: {
city: 'San Francisco',
country: 'USA',
},
},
};
console.log(getHeadquartersCity(techCorp)); // "San Francisco"
console.log(getFullAddress(techCorp)); // "San Francisco, USA"
const startupCo: Company = { name: 'StartupCo' };
console.log(getHeadquartersCity(startupCo)); // "Unknown"
Key Takeaways
- Optional properties use
?: They may or may not exist on the object - Always check optional properties: Use
if,??, or?.before accessing - Readonly prevents modification: Use for IDs, timestamps, and config values
- Readonly arrays cannot be mutated: No push, pop, or index assignment
- Combine optional and readonly: Properties can be both
- Use Partial
: Make all properties optional for update functions - Use Readonly
: Make all properties readonly for immutable objects
Resources
| Resource | Type | Description |
|---|---|---|
| TypeScript Handbook: Optional Properties | Documentation | Official guide |
| TypeScript Handbook: Readonly Properties | Documentation | Official guide |
| TypeScript Handbook: Utility Types | Documentation | Partial, Readonly, and more |
Next Lesson
You now have a complete toolkit for defining types in TypeScript. In the final lesson of this module, we will put everything together in a comprehensive practice exercise.