TypeScript Objects
Introduction
Objects are fundamental building blocks in JavaScript, and TypeScript enhances them with static type checking. In this lesson, we'll explore how TypeScript handles objects, from basic syntax to advanced patterns. Understanding objects in TypeScript is crucial for creating robust, maintainable applications.
Unlike primitive types (strings, numbers, booleans), objects in TypeScript allow you to group related data and functionality together. TypeScript adds the ability to define and enforce the shape of objects, catching potential errors before runtime.
Basic Object Types
In TypeScript, you can define the shape of an object using type annotations.
Simple Object Type Annotation
// Defining an object with type annotations
let person: { name: string; age: number } = {
name: "Alice",
age: 30
};
// Accessing properties
console.log(person.name); // Output: Alice
console.log(person.age); // Output: 30
If you try to add a property that wasn't defined in the type annotation, TypeScript will raise an error:
person.job = "Developer"; // Error: Property 'job' does not exist on type '{ name: string; age: number; }'
Optional Properties
Sometimes an object might have properties that aren't always present. TypeScript lets you mark these as optional using the ?
symbol:
let user: { name: string; email?: string } = {
name: "Bob"
};
// Later we can add the optional property
user.email = "[email protected]";
// This works fine because email is optional
console.log(user); // Output: { name: 'Bob', email: '[email protected]' }
Using Interfaces for Objects
While inline object types are useful, they can become unwieldy for complex objects or when the same object structure is used in multiple places. TypeScript provides interfaces as a powerful way to name and reuse object types.
Basic Interface
interface Person {
name: string;
age: number;
}
let employee: Person = {
name: "Charlie",
age: 35
};
function greet(person: Person) {
return `Hello, ${person.name}!`;
}
console.log(greet(employee)); // Output: Hello, Charlie!
Interface with Optional Properties
interface Product {
id: number;
name: string;
price: number;
description?: string;
inStock?: boolean;
}
let laptop: Product = {
id: 1,
name: "MacBook Pro",
price: 1999
};
let phone: Product = {
id: 2,
name: "iPhone",
price: 999,
description: "Latest model",
inStock: true
};
Readonly Properties
TypeScript allows you to mark properties as readonly, preventing them from being changed after initialization:
interface User {
readonly id: number;
name: string;
}
let admin: User = {
id: 1,
name: "Admin"
};
admin.name = "Super Admin"; // OK
admin.id = 2; // Error: Cannot assign to 'id' because it is a read-only property
Nested Objects
Objects can contain other objects as properties. TypeScript lets you define the structure of these nested objects:
interface Address {
street: string;
city: string;
zipCode: string;
}
interface Customer {
name: string;
address: Address;
contactNumbers: string[];
}
let customer: Customer = {
name: "Dave",
address: {
street: "123 Main St",
city: "Boston",
zipCode: "02108"
},
contactNumbers: ["555-1234", "555-5678"]
};
console.log(customer.address.city); // Output: Boston
console.log(customer.contactNumbers[0]); // Output: 555-1234
Index Signatures
Sometimes you might not know all property names ahead of time, but you do know the shape of the values. Index signatures allow you to define the types for properties that are added dynamically:
interface Dictionary {
[key: string]: string;
}
let colors: Dictionary = {};
colors.red = "#ff0000";
colors.green = "#00ff00";
colors.blue = "#0000ff";
console.log(colors.red); // Output: #ff0000
You can also mix known and index signature properties:
interface Config {
name: string;
[key: string]: string | number;
}
let appConfig: Config = {
name: "MyApp",
version: 1.0,
apiEndpoint: "https://api.example.com"
};
console.log(appConfig.name); // Output: MyApp
console.log(appConfig.version); // Output: 1
Object Methods
Objects can contain methods (functions as properties):
interface Calculator {
add(a: number, b: number): number;
subtract(a: number, b: number): number;
}
const calculator: Calculator = {
add(a, b) {
return a + b;
},
subtract(a, b) {
return a - b;
}
};
console.log(calculator.add(5, 3)); // Output: 8
console.log(calculator.subtract(10, 4)); // Output: 6
Intersection Types
TypeScript allows you to combine multiple object types using intersection types:
interface HasName {
name: string;
}
interface HasAge {
age: number;
}
type PersonInfo = HasName & HasAge;
let personInfo: PersonInfo = {
name: "Eve",
age: 28
};
// personInfo must have both name and age properties
Real-world Example: Product Management System
Let's build a simple product management system using TypeScript objects:
// Define the basic structure for our entities
interface Category {
id: number;
name: string;
}
interface Supplier {
id: number;
name: string;
contactPerson: string;
email: string;
phone: string;
}
interface Product {
id: number;
name: string;
price: number;
category: Category;
supplier: Supplier;
stock: number;
isAvailable: boolean;
lastUpdated: Date;
}
// Create some sample data
const electronics: Category = {
id: 1,
name: "Electronics"
};
const acmeSupplier: Supplier = {
id: 101,
name: "ACME Electronics",
contactPerson: "John Doe",
email: "[email protected]",
phone: "555-1234"
};
const laptop: Product = {
id: 1001,
name: "Premium Laptop",
price: 1299.99,
category: electronics,
supplier: acmeSupplier,
stock: 15,
isAvailable: true,
lastUpdated: new Date()
};
// Function to update stock
function updateStock(product: Product, newStock: number): Product {
return {
...product,
stock: newStock,
isAvailable: newStock > 0,
lastUpdated: new Date()
};
}
// Usage
const updatedLaptop = updateStock(laptop, 10);
console.log(`${updatedLaptop.name} - In stock: ${updatedLaptop.stock}`);
// Output: Premium Laptop - In stock: 10
This example demonstrates how to:
- Define complex object structures using interfaces
- Create relationships between objects
- Use immutable update patterns (returning a new object)
- Work with various types of properties
Advanced Pattern: Discriminated Unions
A common pattern in TypeScript is to use discriminated unions to handle objects that can be of different types:
interface Square {
kind: "square";
size: number;
}
interface Rectangle {
kind: "rectangle";
width: number;
height: number;
}
interface Circle {
kind: "circle";
radius: number;
}
type Shape = Square | Rectangle | Circle;
function calculateArea(shape: Shape): number {
switch (shape.kind) {
case "square":
return shape.size * shape.size;
case "rectangle":
return shape.width * shape.height;
case "circle":
return Math.PI * shape.radius * shape.radius;
}
}
const mySquare: Square = { kind: "square", size: 5 };
const myRectangle: Rectangle = { kind: "rectangle", width: 4, height: 6 };
const myCircle: Circle = { kind: "circle", radius: 3 };
console.log(calculateArea(mySquare)); // Output: 25
console.log(calculateArea(myRectangle)); // Output: 24
console.log(calculateArea(myCircle)); // Output: 28.274333882308138
In this pattern, the kind
property acts as a discriminant, allowing TypeScript to narrow down the type within each case of the switch statement.
Object Type vs Interface
TypeScript offers two main ways to define object types: type aliases and interfaces. Here's a comparison:
// Using an interface
interface UserInterface {
name: string;
age: number;
}
// Using a type alias
type UserType = {
name: string;
age: number;
};
The main differences are:
- Interfaces can be extended using
extends
keyword - Interfaces can be merged if declared multiple times
- Type aliases can create union types and other more complex types
For simple object types, either approach works well and is largely a matter of preference.
Object Destructuring with TypeScript
TypeScript fully supports JavaScript's object destructuring with added type safety:
interface Person {
name: string;
age: number;
address: {
street: string;
city: string;
};
}
function printPersonInfo(person: Person) {
// Destructuring with type safety
const { name, age, address: { city } } = person;
console.log(`${name} is ${age} years old and lives in ${city}`);
}
const person: Person = {
name: "Frank",
age: 40,
address: {
street: "456 Elm St",
city: "Chicago"
}
};
printPersonInfo(person); // Output: Frank is 40 years old and lives in Chicago
Summary
In this lesson, we've covered the essentials of working with objects in TypeScript:
- Basic object type annotations
- Optional and readonly properties
- Using interfaces to define object shapes
- Nested objects and index signatures
- Object methods
- Advanced patterns like intersection types and discriminated unions
- Object destructuring with type safety
TypeScript's strong typing for objects helps catch errors early and provides great tooling support through autocompletion and documentation. By properly defining the shape of your objects, you make your code more robust and easier to maintain.
Exercises
-
Create an interface for a
Book
with properties for title, author, publication year, and an optional property for genre. -
Define an interface for a
TodoItem
and aTodoList
that contains an array ofTodoItem
objects. Implement a function that marks a todo item as complete. -
Create a shopping cart system with
Product
,CartItem
, andCart
interfaces. Implement functions to add items to the cart, update quantities, and calculate the total price. -
Implement a discriminated union for different notification types (email, text message, push notification) with appropriate properties for each type.
Additional Resources
- TypeScript Handbook: Objects
- Interface vs. Type Alias
- TypeScript Deep Dive: Object Oriented Programming
Understanding objects in TypeScript provides the foundation for object-oriented programming and more complex application development. Practice working with objects to become proficient in TypeScript development.
If you spot any mistakes on this website, please let me know at [email protected]. I’d greatly appreciate your feedback! :)