Object Types
Object Types
Object types are fundamental to TypeScript, allowing you to describe the shape and structure of objects in your code. Understanding how to properly type objects is essential for building type-safe applications. TypeScript provides various ways to define and work with object types, from simple property annotations to advanced features like index signatures and readonly properties.
Object Type Annotation
The simplest way to type an object is by using object type annotation inline:
// Inline object type
let user: {
name: string;
age: number;
email: string;
} = {
name: 'Alice',
age: 30,
email: 'alice@example.com'
};
// Function parameter with object type
function displayUser(user: {
name: string;
age: number;
isActive: boolean;
}): void {
console.log(`User: ${user.name}, Age: ${user.age}`);
console.log(`Status: ${user.isActive ? 'Active' : 'Inactive'}`);
}
displayUser({
name: 'Bob',
age: 25,
isActive: true
});
// Function returning object type
function createProduct(
id: string,
name: string,
price: number
): {
id: string;
name: string;
price: number;
createdAt: Date;
} {
return {
id,
name,
price,
createdAt: new Date()
};
}
const product = createProduct('prod-001', 'Laptop', 999);
console.log(product.createdAt); // TypeScript knows this is a Date
Optional Properties
Optional properties allow certain properties to be present or absent in an object. Mark properties as optional using the question mark (?):
// Type with optional properties
type UserProfile = {
username: string;
email: string;
bio?: string; // Optional
website?: string; // Optional
phone?: string; // Optional
};
// Valid: includes all required properties
const user1: UserProfile = {
username: 'john_doe',
email: 'john@example.com'
};
// Valid: includes optional properties
const user2: UserProfile = {
username: 'jane_smith',
email: 'jane@example.com',
bio: 'Software developer',
website: 'https://janesmith.dev'
};
// Function handling optional properties
function displayProfile(profile: UserProfile): void {
console.log(`Username: ${profile.username}`);
console.log(`Email: ${profile.email}`);
// Check if optional property exists
if (profile.bio) {
console.log(`Bio: ${profile.bio}`);
}
// Optional chaining with optional properties
console.log(`Website: ${profile.website ?? 'Not provided'}`);
}
// Nested optional properties
type Address = {
street: string;
city: string;
state: string;
zipCode: string;
country?: string;
};
type Customer = {
name: string;
email: string;
address?: Address; // Entire address is optional
};
const customer1: Customer = {
name: 'Alice',
email: 'alice@example.com'
};
const customer2: Customer = {
name: 'Bob',
email: 'bob@example.com',
address: {
street: '123 Main St',
city: 'New York',
state: 'NY',
zipCode: '10001',
country: 'USA'
}
};
?.) and nullish coalescing (??) operators to safely access and provide defaults for optional properties.
Readonly Properties
Readonly properties can only be assigned during initialization and cannot be modified afterward. Use the readonly modifier:
// Type with readonly properties
type Config = {
readonly apiKey: string;
readonly baseUrl: string;
readonly timeout: number;
retryAttempts: number; // Not readonly, can be modified
};
const config: Config = {
apiKey: 'secret-key-123',
baseUrl: 'https://api.example.com',
timeout: 5000,
retryAttempts: 3
};
// Valid: modifying non-readonly property
config.retryAttempts = 5;
// Error: Cannot assign to readonly property
// config.apiKey = 'new-key';
// config.baseUrl = 'https://new-api.com';
// Readonly with nested objects
type Article = {
readonly id: string;
title: string;
content: string;
readonly metadata: {
readonly createdAt: Date;
readonly author: string;
views: number; // Not readonly
};
};
const article: Article = {
id: 'article-001',
title: 'TypeScript Guide',
content: 'Learn TypeScript...',
metadata: {
createdAt: new Date(),
author: 'John Doe',
views: 0
}
};
// Valid: modifying non-readonly nested property
article.metadata.views = 100;
// Valid: modifying non-readonly top-level property
article.title = 'Complete TypeScript Guide';
// Error: Cannot modify readonly properties
// article.id = 'new-id';
// article.metadata.createdAt = new Date();
// article.metadata.author = 'Jane Doe';
// Readonly arrays
type TodoList = {
readonly items: readonly string[];
};
const todos: TodoList = {
items: ['Task 1', 'Task 2', 'Task 3']
};
// Error: Cannot modify readonly array
// todos.items.push('Task 4');
// todos.items[0] = 'Updated Task';
readonly is a compile-time check only. It doesn't make objects immutable at runtime. If you need true immutability, consider using Object.freeze() or immutable data structures.
Index Signatures
Index signatures allow you to describe objects that can have properties with dynamic keys. This is useful when you don't know all property names in advance:
// String index signature
type StringDictionary = {
[key: string]: string;
};
const translations: StringDictionary = {
hello: 'Hola',
goodbye: 'Adiós',
thanks: 'Gracias'
};
// Add new properties dynamically
translations.welcome = 'Bienvenido';
console.log(translations.hello); // "Hola"
// Number index signature
type NumberArray = {
[index: number]: string;
};
const fruits: NumberArray = {
0: 'Apple',
1: 'Banana',
2: 'Orange'
};
console.log(fruits[1]); // "Banana"
// Index signature with known properties
type UserData = {
name: string; // Known property
email: string; // Known property
[key: string]: string; // Additional dynamic properties
};
const userData: UserData = {
name: 'Alice',
email: 'alice@example.com',
phone: '+1234567890', // Dynamic property
address: '123 Main St' // Dynamic property
};
// Mixed value types with index signature
type MixedData = {
[key: string]: string | number | boolean;
};
const settings: MixedData = {
theme: 'dark',
fontSize: 16,
autoSave: true,
language: 'en'
};
// Readonly index signature
type ReadonlyDictionary = {
readonly [key: string]: number;
};
const scores: ReadonlyDictionary = {
math: 95,
science: 88,
history: 92
};
// Error: Cannot modify properties in readonly index signature
// scores.math = 100;
[key: string]: number, you cannot add a property with a string value.
Excess Property Checks
TypeScript performs excess property checks when assigning object literals to typed variables. This prevents typos and ensures objects have only the expected properties:
type User = {
name: string;
age: number;
};
// Valid: object has exactly the expected properties
const user1: User = {
name: 'Alice',
age: 30
};
// Error: Excess property 'email' not in type 'User'
// const user2: User = {
// name: 'Bob',
// age: 25,
// email: 'bob@example.com' // Excess property
// };
// Workaround 1: Type assertion (use sparingly)
const user3: User = {
name: 'Charlie',
age: 35,
email: 'charlie@example.com'
} as User;
// Workaround 2: Assign to variable first
const tempUser = {
name: 'David',
age: 28,
email: 'david@example.com'
};
const user4: User = tempUser; // No error, excess property check bypassed
// Workaround 3: Use index signature
type FlexibleUser = {
name: string;
age: number;
[key: string]: unknown; // Allow additional properties
};
const user5: FlexibleUser = {
name: 'Eve',
age: 32,
email: 'eve@example.com', // Now allowed
phone: '+1234567890' // Now allowed
};
// Function parameters also trigger excess property checks
function createUser(user: User): void {
console.log(`Created user: ${user.name}`);
}
// Error: Excess property 'email'
// createUser({
// name: 'Frank',
// age: 40,
// email: 'frank@example.com'
// });
// Valid: exact properties
createUser({
name: 'Grace',
age: 27
});
Nested Object Types
Objects can contain other objects, creating nested structures:
// Deeply nested object type
type Company = {
name: string;
address: {
street: string;
city: string;
country: string;
coordinates: {
latitude: number;
longitude: number;
};
};
employees: {
count: number;
departments: {
name: string;
headCount: number;
}[];
};
contact: {
email: string;
phone: string;
social: {
twitter?: string;
linkedin?: string;
github?: string;
};
};
};
const company: Company = {
name: 'Tech Corp',
address: {
street: '100 Tech Avenue',
city: 'San Francisco',
country: 'USA',
coordinates: {
latitude: 37.7749,
longitude: -122.4194
}
},
employees: {
count: 500,
departments: [
{ name: 'Engineering', headCount: 200 },
{ name: 'Sales', headCount: 150 },
{ name: 'Marketing', headCount: 100 }
]
},
contact: {
email: 'info@techcorp.com',
phone: '+1-555-0123',
social: {
twitter: '@techcorp',
linkedin: 'techcorp'
}
}
};
// Accessing nested properties
console.log(company.address.city); // "San Francisco"
console.log(company.address.coordinates.latitude); // 37.7749
console.log(company.employees.departments[0].name); // "Engineering"
console.log(company.contact.social.twitter); // "@techcorp"
// Optional chaining with nested objects
console.log(company.contact.social.github ?? 'Not provided');
Object Type Utilities
TypeScript provides built-in utility types for working with object types:
// Original type
type User = {
id: string;
name: string;
email: string;
age: number;
isActive: boolean;
};
// Partial<T> - Makes all properties optional
type PartialUser = Partial<User>;
const updateUser: PartialUser = {
name: 'Alice',
age: 31
// Other properties are optional
};
// Required<T> - Makes all properties required
type OptionalUser = {
name?: string;
email?: string;
age?: number;
};
type RequiredUser = Required<OptionalUser>;
const fullUser: RequiredUser = {
name: 'Bob',
email: 'bob@example.com',
age: 25 // All properties now required
};
// Readonly<T> - Makes all properties readonly
type ReadonlyUser = Readonly<User>;
const user: ReadonlyUser = {
id: '123',
name: 'Charlie',
email: 'charlie@example.com',
age: 30,
isActive: true
};
// Error: Cannot modify readonly properties
// user.name = 'David';
// Pick<T, K> - Creates type with subset of properties
type UserPreview = Pick<User, 'id' | 'name' | 'email'>;
const preview: UserPreview = {
id: '456',
name: 'Eve',
email: 'eve@example.com'
// age and isActive not needed
};
// Omit<T, K> - Creates type excluding specified properties
type UserWithoutId = Omit<User, 'id' | 'isActive'>;
const newUser: UserWithoutId = {
name: 'Frank',
email: 'frank@example.com',
age: 35
// id and isActive excluded
};
// Record<K, T> - Creates object type with specific keys and value type
type UserRoles = Record<string, boolean>;
const roles: UserRoles = {
admin: true,
editor: false,
viewer: true
};
// Combining utilities
type PartialReadonlyUser = Partial<Readonly<User>>;
type RequiredUserPreview = Required<Pick<User, 'name' | 'email'>>;
Object Destructuring with Types
TypeScript works seamlessly with object destructuring:
type Product = {
id: string;
name: string;
price: number;
category: string;
inStock: boolean;
};
// Destructuring in function parameters
function displayProduct({ name, price, inStock }: Product): void {
console.log(`Product: ${name}`);
console.log(`Price: $${price}`);
console.log(`In Stock: ${inStock ? 'Yes' : 'No'}`);
}
const laptop: Product = {
id: 'prod-001',
name: 'MacBook Pro',
price: 2499,
category: 'Electronics',
inStock: true
};
displayProduct(laptop);
// Destructuring with renaming
function processUser({ name: userName, email: userEmail }: {
name: string;
email: string;
}): void {
console.log(`User: ${userName}`);
console.log(`Email: ${userEmail}`);
}
// Nested destructuring
type Order = {
id: string;
customer: {
name: string;
email: string;
};
items: {
product: string;
quantity: number;
}[];
};
function processOrder({
id,
customer: { name, email },
items
}: Order): void {
console.log(`Order ID: ${id}`);
console.log(`Customer: ${name} (${email})`);
console.log(`Items: ${items.length}`);
}
- Create a type
Bookwith properties:readonly id: string,title: string,author: string,publishedYear: number, optionalisbn?: string, and optionaltags?: string[] - Create a type
Librarywith a string index signature that maps book IDs toBookobjects, plus a propertyname: string - Create a function
addBookthat takes aLibraryand aBook, adds the book to the library, and returns the updated library - Create a type
BookSummaryusingPickto extract onlytitle,author, andpublishedYearfromBook - Create instances of
Book,Library, and test youraddBookfunction
Summary
- Object type annotation describes the shape and structure of objects
- Optional properties use
?to make properties optional - Readonly properties use
readonlyto prevent modification after initialization - Index signatures allow objects with dynamic property names
- Excess property checks prevent typos by rejecting unexpected properties in object literals
- Nested objects create complex hierarchical data structures
- Utility types like
Partial,Required,Readonly,Pick, andOmittransform object types - Destructuring works seamlessly with TypeScript types