Objects
Introduction
Objects are one of the most important concepts in programming. They let you group related data and functionality together into a single unit. Think of an object as a container that holds information (properties) and actions (methods) related to a real-world entity.
For example, a person object might have:
- Properties : name, age, email (data)
- Methods : greet(), updateProfile() (actions)
In TypeScript, objects become even more powerful because you can define the exact structure an object should have. This prevents bugs where you accidentally access properties that don't exist or assign the wrong type of value.
Why Objects Matter
Objects help you organize complex data:
Without objects (messy):
let userName: string = "Alice";
let userAge: number = 25;
let userEmail: string = "alice@email.com";
let userCity: string = "London";
// What if you need to pass this data around? Pass 4 separate variables?
With objects (organized):
let user = {
name: "Alice",
age: 25,
email: "alice@email.com",
city: "London"
};
// Pass one object that contains everything!
TypeScript's Object Type Safety
The major difference in TypeScript is that you can define the shape of your objects:
JavaScript (no type checking):
let user = { name: "Alice", age: 25 };
console.log(user.email); // undefined - typo! Should be checking if it exists
user.age = "twenty-five"; // Oops! Changed type from number to string
TypeScript (with type checking):
interface User {
name: string;
age: number;
email?: string; // Optional property
}
let user: User = { name: "Alice", age: 25 };
// console.log(user.emial); // ❌ Error: Property 'emial' does not exist (catches typo!)
// user.age = "twenty-five"; // ❌ Error: Type 'string' is not assignable to type 'number'
Creating Objects
Object Literals
The most common way to create objects:
let person = {
name: "Alice",
age: 25,
city: "London"
};
console.log(person); // { name: 'Alice', age: 25, city: 'London' }
With Type Annotations
You can specify the object's shape:
let person: { name: string; age: number; city: string } = {
name: "Alice",
age: 25,
city: "London"
};
// TypeScript checks this matches!
// let invalid: { name: string; age: number } = {
// name: "Bob" // ❌ Error: Property 'age' is missing
// };
Using Interfaces
For reusable object shapes (recommended for complex objects):
interface Person {
name: string;
age: number;
city: string;
}
let alice: Person = {
name: "Alice",
age: 25,
city: "London"
};
let bob: Person = {
name: "Bob",
age: 30,
city: "Paris"
};
Using Type Aliases
Similar to interfaces:
type Person = {
name: string;
age: number;
city: string;
};
let person: Person = {
name: "Alice",
age: 25,
city: "London"
};
When to use Interface vs Type?
- Use
interfacefor object shapes (more extensible) - Use
typefor unions, primitives, or complex types - Either works for simple objects - be consistent!
Accessing Properties
Dot Notation
The most common way:
let person = {
name: "Alice",
age: 25
};
console.log(person.name); // "Alice"
console.log(person.age); // 25
Bracket Notation
Useful for dynamic property names or special characters:
let person = {
name: "Alice",
"favorite color": "blue" // Space in key
};
// Must use brackets for special characters
console.log(person["favorite color"]); // "blue"
// Dynamic property access
let propertyName: string = "name";
console.log(person[propertyName]); // "Alice"
// With variables
let key: string = "age";
// console.log(person[key]); // Would work if person had 'age'
Type Safety with Property Access
TypeScript ensures properties exist:
interface User {
name: string;
age: number;
}
let user: User = { name: "Alice", age: 25 };
console.log(user.name); // ✅ "Alice"
// console.log(user.email); // ❌ Error: Property 'email' does not exist
Optional Properties
Mark properties as optional with ?:
interface User {
name: string;
age: number;
email?: string; // Optional
phone?: string; // Optional
}
// Valid - optional properties can be omitted
let user1: User = {
name: "Alice",
age: 25
};
// Also valid - optional properties can be included
let user2: User = {
name: "Bob",
age: 30,
email: "bob@email.com"
};
Accessing Optional Properties Safely
interface User {
name: string;
email?: string;
}
let user: User = { name: "Alice" };
// Safe access with optional chaining
console.log(user.email?.toLowerCase()); // undefined (no error!)
// Check before using
if (user.email) {
console.log(user.email.toLowerCase());
}
// With default value
let email: string = user.email ?? "No email provided";
Adding and Modifying Properties
Adding Properties
interface Person {
name: string;
age: number;
city?: string; // Optional so we can add it later
}
let person: Person = {
name: "Alice",
age: 25
};
person.city = "London"; // ✅ Add optional property
console.log(person); // { name: 'Alice', age: 25, city: 'London' }
// person.country = "UK"; // ❌ Error: Property 'country' does not exist
Modifying Properties
let person = {
name: "Alice",
age: 25
};
person.age = 26; // ✅ Modify property
console.log(person.age); // 26
// person.age = "twenty-six"; // ❌ Error: Type 'string' not assignable
Dynamic Properties with Index Signatures
For objects with dynamic keys:
interface StringMap {
[key: string]: string;
}
let translations: StringMap = {};
translations["hello"] = "hola";
translations["goodbye"] = "adiós";
translations["thanks"] = "gracias";
console.log(translations["hello"]); // "hola"
// More specific example
interface UserScores {
[username: string]: number;
}
let scores: UserScores = {
alice: 100,
bob: 85,
charlie: 92
};
Readonly Properties
Prevent modification after creation:
interface User {
readonly id: number;
name: string;
age: number;
}
let user: User = {
id: 1,
name: "Alice",
age: 25
};
user.name = "Alicia"; // ✅ Can modify
// user.id = 2; // ❌ Error: Cannot assign to 'id' because it is a read-only property
Methods
Objects can contain functions (methods):
interface Person {
name: string;
age: number;
greet: () => string;
}
let person: Person = {
name: "Alice",
age: 25,
greet: function() {
return `Hello, I'm ${this.name}!`;
}
};
console.log(person.greet()); // "Hello, I'm Alice!"
// Shorthand method syntax (preferred)
let person2 = {
name: "Bob",
age: 30,
greet() {
return `Hi, I'm ${this.name}!`;
}
};
The this Keyword
this refers to the object the method belongs to:
let calculator = {
value: 0,
add(n: number) {
this.value += n;
return this; // Return this for chaining
},
subtract(n: number) {
this.value -= n;
return this;
},
getValue() {
return this.value;
}
};
calculator.add(10).add(5).subtract(3);
console.log(calculator.getValue()); // 12
// Method chaining works!
Arrow Functions and this
Arrow functions don't have their own this:
let person = {
name: "Alice",
greet: function() {
console.log(`Hello, ${this.name}`); // ✅ 'this' works
},
arrowGreet: () => {
// console.log(`Hello, ${this.name}`); // ❌ 'this' is undefined!
}
};
// Use regular functions for methods that need 'this'
Deleting Properties
Remove properties with delete:
let person = {
name: "Alice",
age: 25,
city: "London"
};
delete person.city;
console.log(person); // { name: 'Alice', age: 25 }
// Check if property exists
console.log("city" in person); // false
Nested Objects
Objects can contain other objects:
interface Address {
street: string;
city: string;
country: string;
}
interface Person {
name: string;
age: number;
address: Address;
}
let person: Person = {
name: "Alice",
age: 25,
address: {
street: "123 Main St",
city: "London",
country: "UK"
}
};
// Access nested properties
console.log(person.address.city); // "London"
console.log(person.address.country); // "UK"
// Optional chaining with nested objects
interface User {
name: string;
address?: {
city?: string;
};
}
let user: User = { name: "Bob" };
console.log(user.address?.city); // undefined (no error!)
Object Destructuring
Extract properties into variables:
let person = {
name: "Alice",
age: 25,
city: "London"
};
// Destructuring
let { name, age, city } = person;
console.log(name); // "Alice"
console.log(age); // 25
console.log(city); // "London"
// Rename during destructuring
let { name: personName, age: personAge } = person;
console.log(personName); // "Alice"
// Default values
let { name, country = "Unknown" } = person;
console.log(country); // "Unknown"
// Nested destructuring
let user = {
name: "Bob",
address: {
city: "Paris",
country: "France"
}
};
let { name, address: { city, country } } = user;
console.log(city); // "Paris"
console.log(country); // "France"
Object Spreading
Copy and merge objects:
let person = {
name: "Alice",
age: 25
};
// Copy object
let personCopy = { ...person };
console.log(personCopy); // { name: 'Alice', age: 25 }
// Add properties
let extendedPerson = { ...person, city: "London" };
console.log(extendedPerson); // { name: 'Alice', age: 25, city: 'London' }
// Merge objects (later properties override earlier ones)
let defaults = { theme: "light", fontSize: 14 };
let userSettings = { fontSize: 16, language: "en" };
let settings = { ...defaults, ...userSettings };
console.log(settings); // { theme: 'light', fontSize: 16, language: 'en' }
Object Methods (Built-in)
Object.keys()
Get array of property names:
let person = {
name: "Alice",
age: 25,
city: "London"
};
let keys: string[] = Object.keys(person);
console.log(keys); // ["name", "age", "city"]
Object.values()
Get array of property values:
let values: any[] = Object.values(person);
console.log(values); // ["Alice", 25, "London"]
Object.entries()
Get array of [key, value] pairs:
let entries: [string, any][] = Object.entries(person);
console.log(entries);
// [["name", "Alice"], ["age", 25], ["city", "London"]]
// Useful for iteration
for (let [key, value] of entries) {
console.log(`${key}: ${value}`);
}
Object.assign()
Copy properties from source to target:
let target = { name: "Alice" };
let source = { age: 25, city: "London" };
Object.assign(target, source);
console.log(target); // { name: 'Alice', age: 25, city: 'London' }
// Spread is usually preferred
let merged = { ...target, ...source };
Type Aliases vs Interfaces
Both define object shapes, but have differences:
Interfaces
interface Person {
name: string;
age: number;
}
// Can extend
interface Employee extends Person {
employeeId: string;
department: string;
}
let emp: Employee = {
name: "Alice",
age: 25,
employeeId: "E123",
department: "Engineering"
};
// Can merge (declaration merging)
interface User {
name: string;
}
interface User {
age: number;
}
// User now has both name and age
Type Aliases
type Person = {
name: string;
age: number;
};
// Can use with unions
type ID = string | number;
// Can use with intersections
type Employee = Person & {
employeeId: string;
department: string;
};
Recommendation: Use interfaces for object shapes, types for everything else.
Concepts
Quick Reference
// Creating objects
let obj = { key: "value" };
// With type annotation
let person: { name: string; age: number } = {
name: "Alice",
age: 25
};
// With interface
interface User {
name: string;
age: number;
email?: string; // Optional
readonly id: number; // Readonly
}
// Accessing properties
obj.key // Dot notation
obj["key"] // Bracket notation
// Methods
let calc = {
value: 0,
add(n: number) {
this.value += n;
}
};
// Destructuring
let { name, age } = person;
// Spreading
let copy = { ...person };
let merged = { ...obj1, ...obj2 };
// Object methods
Object.keys(obj) // ["key"]
Object.values(obj) // ["value"]
Object.entries(obj) // [["key", "value"]]
Examples
// User profile system
interface UserProfile {
id: number;
username: string;
email: string;
preferences: {
theme: "light" | "dark";
notifications: boolean;
};
getDisplayName: () => string;
}
let user: UserProfile = {
id: 1,
username: "alice_dev",
email: "alice@example.com",
preferences: {
theme: "dark",
notifications: true
},
getDisplayName() {
return `@${this.username}`;
}
};
// Product catalog
interface Product {
id: string;
name: string;
price: number;
inStock: boolean;
calculateDiscount: (percent: number) => number;
}
let laptop: Product = {
id: "LAP001",
name: "Gaming Laptop",
price: 1200,
inStock: true,
calculateDiscount(percent: number) {
return this.price * (1 - percent / 100);
}
};
console.log(laptop.calculateDiscount(10)); // 1080
Challenges
1. Create Student Object
- Create an object
studentwith properties: name, age, and course. - Use proper TypeScript type annotations.
- Print each property individually.
💡 Show Answer
interface Student {
name: string;
age: number;
course: string;
}
let student: Student = {
name: "Alice",
age: 21,
course: "Computer Science"
};
console.log(student.name); // "Alice"
console.log(student.age); // 21
console.log(student.course); // "Computer Science"
// Alternative with type alias
type Student2 = {
name: string;
age: number;
course: string;
};
// Function to create student
function createStudent(name: string, age: number, course: string): Student {
return { name, age, course };
}
let bob = createStudent("Bob", 22, "Engineering");
console.log(bob);
2. Add and Update Properties
- Add a new property
gradeto the student object. - Update the student's age.
- Make grade an optional property in the type definition.
💡 Show Answer
interface Student {
name: string;
age: number;
course: string;
grade?: string; // Optional property
}
let student: Student = {
name: "Alice",
age: 21,
course: "Computer Science"
};
// Add grade
student.grade = "A";
console.log(student.grade); // "A"
// Update age
student.age = 22;
console.log(student.age); // 22
console.log(student);
// { name: 'Alice', age: 22, course: 'Computer Science', grade: 'A' }
// Function to update student
function updateStudentAge(student: Student, newAge: number): void {
student.age = newAge;
}
updateStudentAge(student, 23);
console.log(student.age); // 23
3. Delete Property
- Create a student object with an
isGraduateproperty. - Delete the
isGraduateproperty. - Verify it's gone using
inoperator or Object.keys().
💡 Show Answer
interface Student {
name: string;
age: number;
isGraduate?: boolean;
}
let student: Student = {
name: "Alice",
age: 21,
isGraduate: false
};
console.log("Before delete:", student);
// { name: 'Alice', age: 21, isGraduate: false }
delete student.isGraduate;
console.log("After delete:", student);
// { name: 'Alice', age: 21 }
// Verify deletion
console.log("isGraduate" in student); // false
console.log(Object.keys(student)); // ["name", "age"]
// Safe check before access
if (student.isGraduate !== undefined) {
console.log("Is graduate:", student.isGraduate);
} else {
console.log("Graduate status unknown");
}
4. Add Method getSummary
- Create a
Bookinterface with title and author. - Add a method
getSummary()that returns a formatted string. - Create a book object and call the method.
💡 Show Answer
interface Book {
title: string;
author: string;
year?: number;
getSummary: () => string;
}
let book: Book = {
title: "Harry Potter and the Philosopher's Stone",
author: "J.K. Rowling",
year: 1997,
getSummary() {
if (this.year) {
return `"${this.title}" by ${this.author} (${this.year})`;
}
return `"${this.title}" by ${this.author}`;
}
};
console.log(book.getSummary());
// "Harry Potter and the Philosopher's Stone" by J.K. Rowling (1997)
// Array of books
let library: Book[] = [
{
title: "1984",
author: "George Orwell",
getSummary() {
return `"${this.title}" by ${this.author}`;
}
},
{
title: "To Kill a Mockingbird",
author: "Harper Lee",
getSummary() {
return `"${this.title}" by ${this.author}`;
}
}
];
library.forEach(book => console.log(book.getSummary()));
5. Bracket Notation with Special Keys
- Create an object with a property name that contains a space.
- Access it using bracket notation.
- Demonstrate why dot notation won't work.
💡 Show Answer
interface Person {
name: string;
age: number;
"favorite color": string; // Key with space
"home address"?: string;
}
let person: Person = {
name: "Alice",
age: 25,
"favorite color": "blue"
};
// Access with bracket notation
console.log(person["favorite color"]); // "blue"
// Dot notation won't work
// console.log(person.favorite color); // ❌ Syntax error
// Dynamic key access
let key: string = "favorite color";
console.log(person[key]); // "blue"
// Adding property with space
person["home address"] = "123 Main St";
console.log(person["home address"]);
// Index signature for flexible keys
interface FlexibleObject {
[key: string]: string | number | boolean;
name: string; // Required property
}
let obj: FlexibleObject = {
name: "Test",
"property with space": "value",
"another-property": 123
};
6. Object Destructuring
- Create a car object with make, year, and color.
- Use destructuring to extract all properties.
- Rename a property during destructuring.
- Provide default values for missing properties.
💡 Show Answer
interface Car {
make: string;
year: number;
color: string;
owner?: string;
}
let car: Car = {
make: "Toyota",
year: 2020,
color: "Blue"
};
// Basic destructuring
let { make, year, color } = car;
console.log(make); // "Toyota"
console.log(year); // 2020
console.log(color); // "Blue"
// Rename during destructuring
let { make: carMake, year: carYear } = car;
console.log(carMake); // "Toyota"
console.log(carYear); // 2020
// Default values
let { make: m, owner = "Unknown" } = car;
console.log(m); // "Toyota"
console.log(owner); // "Unknown"
// Destructuring in function parameters
function displayCar({ make, year, color }: Car): string {
return `${year} ${make} (${color})`;
}
console.log(displayCar(car)); // "2020 Toyota (Blue)"
// Rest operator in destructuring
let { make: carBrand, ...rest } = car;
console.log(carBrand); // "Toyota"
console.log(rest); // { year: 2020, color: 'Blue' }
7. Nested Objects
- Create a company object with nested address object.
- Create a proper interface for the nested structure.
- Access nested properties safely.
💡 Show Answer
interface Address {
street: string;
city: string;
country: string;
zipCode?: string;
}
interface Company {
name: string;
founded: number;
address: Address;
employees?: number;
}
let company: Company = {
name: "TechCorp",
founded: 2010,
address: {
street: "123 Tech Street",
city: "San Francisco",
country: "USA",
zipCode: "94105"
},
employees: 500
};
// Access nested properties
console.log(company.name); // "TechCorp"
console.log(company.address.city); // "San Francisco"
console.log(company.address.country); // "USA"
// Nested destructuring
let { name, address: { city, country } } = company;
console.log(`${name} is in ${city}, ${country}`);
// "TechCorp is in San Francisco, USA"
// Optional chaining with nested objects
interface User {
name: string;
contact?: {
email?: string;
address?: {
city?: string;
};
};
}
let user: User = { name: "Bob" };
console.log(user.contact?.address?.city); // undefined (no error!)
// Deep nesting example
let library = {
name: "City Library",
sections: {
fiction: {
books: [
{ title: "1984", author: "George Orwell" },
{ title: "Brave New World", author: "Aldous Huxley" }
]
},
nonFiction: {
books: [
{ title: "Sapiens", author: "Yuval Noah Harari" }
]
}
}
};
console.log(library.sections.fiction.books[0].title); // "1984"
8. Count Properties
- Write a function that counts the number of properties in an object.
- Make it type-safe with generics.
- Handle nested objects differently if needed.
💡 Show Answer
// Basic version
function countProperties(obj: object): number {
return Object.keys(obj).length;
}
let student = {
name: "Alice",
age: 21,
course: "Computer Science",
grade: "A"
};
console.log(countProperties(student)); // 4
// Generic version
function countPropertiesGeneric<T extends object>(obj: T): number {
return Object.keys(obj).length;
}
console.log(countPropertiesGeneric(student)); // 4
// Count only own properties (not inherited)
function countOwnProperties(obj: object): number {
return Object.getOwnPropertyNames(obj).length;
}
// Count nested properties recursively
function countAllProperties(obj: any): number {
let count = 0;
for (let key in obj) {
count++;
if (typeof obj[key] === "object" && obj[key] !== null) {
count += countAllProperties(obj[key]);
}
}
return count;
}
let nested = {
name: "Alice",
address: {
city: "London",
country: "UK"
}
};
console.log(countProperties(nested)); // 2 (name, address)
console.log(countAllProperties(nested)); // 4 (name, address, city, country)
// With type information
function analyzeObject<T extends object>(obj: T): {
propertyCount: number;
properties: string[];
} {
const properties = Object.keys(obj);
return {
propertyCount: properties.length,
properties
};
}
console.log(analyzeObject(student));
// { propertyCount: 4, properties: ['name', 'age', 'course', 'grade'] }
9. Loop Through Object
- Use
for...into loop through object properties. - Use
Object.entries()for a more modern approach. - Create a function to display all key-value pairs.
💡 Show Answer
interface Student {
name: string;
age: number;
course: string;
isGraduate: boolean;
}
let student: Student = {
name: "Alice",
age: 21,
course: "Computer Science",
isGraduate: false
};
// Using for...in
console.log("Using for...in:");
for (let key in student) {
console.log(`${key}: ${student[key as keyof Student]}`);
}
// Using Object.entries() (preferred)
console.log("\nUsing Object.entries():");
for (let [key, value] of Object.entries(student)) {
console.log(`${key}: ${value}`);
}
// Using Object.keys()
console.log("\nUsing Object.keys():");
Object.keys(student).forEach(key => {
console.log(`${key}: ${student[key as keyof Student]}`);
});
// Function to display object
function displayObject `<T extends object>`(obj: T, title?: string): void {
if (title) console.log(`\n${title}:`);
Object.entries(obj).forEach(([key, value]) => {
console.log(` ${key}: ${value}`);
});
}
displayObject(student, "Student Information");
// Filter properties
function getStringProperties `<T extends object>`(obj: T): Record<string, string> {
const result: Record<string, string> = {};
for (let [key, value] of Object.entries(obj)) {
if (typeof value === "string") {
result[key] = value;
}
}
return result;
}
console.log(getStringProperties(student));
// { name: 'Alice', course: 'Computer Science' }
// Map object properties
function mapObjectValues `<T extends object>`(
obj: T,
fn: (value: any) => any
): Record<string, any> {
const result: Record<string, any> = {};
for (let [key, value] of Object.entries(obj)) {
result[key] = fn(value);
}
return result;
}
let doubled = mapObjectValues({ a: 1, b: 2, c: 3 }, x => x * 2);
console.log(doubled); // { a: 2, b: 4, c: 6 }
10. Merge Objects with Spread
- Merge two objects using the spread operator.
- Handle property conflicts (same property in both objects).
- Create a function that merges objects with conflict resolution.
💡 Show Answer
interface ContactInfo {
name: string;
email: string;
phone: string;
}
interface PersonalInfo {
name: string;
address: string;
city: string;
country: string;
}
let contactInfo: ContactInfo = {
name: "Alice Johnson",
email: "alice@email.com",
phone: "555-0100"
};
let personalInfo: PersonalInfo = {
name: "Alice J.", // Different name - will override
address: "123 Main St",
city: "London",
country: "UK"
};
// Simple merge (later values override)
let merged1 = { ...contactInfo, ...personalInfo };
console.log(merged1);
// name is "Alice J." from personalInfo
// Reverse order
let merged2 = { ...personalInfo, ...contactInfo };
console.log(merged2);
// name is "Alice Johnson" from contactInfo
// Nested merge strategy
interface MergedInfo {
contact: ContactInfo;
personal: PersonalInfo;
}
let structuredMerge: MergedInfo = {
contact: { ...contactInfo },
personal: { ...personalInfo }
};
console.log(structuredMerge);
// Both objects preserved with their own names
// Custom merge function
function mergeObjects<T extends object, U extends object>(
obj1: T,
obj2: U,
strategy: "first" | "second" | "nested" = "second"
): any {
if (strategy === "nested") {
return {
first: { ...obj1 },
second: { ...obj2 }
};
} else if (strategy === "first") {
return { ...obj2, ...obj1 }; // obj1 wins conflicts
} else {
return { ...obj1, ...obj2 }; // obj2 wins conflicts
}
}
console.log(mergeObjects(contactInfo, personalInfo, "first"));
// name is "Alice Johnson"
console.log(mergeObjects(contactInfo, personalInfo, "nested"));
// Preserves both in separate properties
// Deep merge function
function deepMerge<T extends object>(target: T, source: Partial<T>): T {
const result = { ...target };
for (let key in source) {
if (source[key] !== undefined) {
result[key] = source[key]!;
}
}
return result;
}
// Merge multiple objects
function mergeMultiple<T extends object>(...objects: Partial<T>[]): T {
return objects.reduce((acc, obj) => ({ ...acc, ...obj }), {} as T);
}
let settings1 = { theme: "dark", fontSize: 14 };
let settings2 = { fontSize: 16, language: "en" };
let settings3 = { notifications: true };
let allSettings = mergeMultiple(settings1, settings2, settings3);
console.log(allSettings);
// { theme: 'dark', fontSize: 16, language: 'en', notifications: true }
Summary
In this chapter, you learned:
✅ How to create objects with object literals and type annotations
✅ Defining object shapes with interfaces and type aliases
✅ Accessing properties with dot and bracket notation
✅ Optional and readonly properties
✅ Adding, modifying, and deleting properties
✅ Creating methods inside objects
✅ Understanding the this keyword
✅ Nested objects and safe property access
✅ Object destructuring for cleaner code
✅ Spreading objects for copying and merging
✅ Built-in object methods (keys, values, entries)
Key Takeaways:
- Objects group related data and behavior together
- TypeScript's interfaces and types define object shapes for safety
- Use optional properties with
?for flexibility - Use readonly for properties that shouldn't change
- Dot notation for known properties, bracket notation for dynamic access
- Methods let objects have behavior, not just data
- Destructuring and spreading make object manipulation cleaner
- Optional chaining (
?.) prevents errors with nested objects
Best Practices:
- Use interfaces for object shapes (more extensible than types)
- Make properties optional only when they truly are optional
- Use readonly for IDs and other immutable data
- Prefer destructuring for cleaner function parameters
- Use the spread operator for shallow copying
- Use descriptive property names
- Group related data in nested objects
- Validate object shapes at boundaries (API responses, user input)
In the next chapter, we'll explore JSON and learn how to work with data interchange formats in a type-safe way!
Continue to Chapter 7: JSON →