Objects are fundamental to JavaScript and TypeScript. Unlike primitive types that hold single values, objects can hold multiple related values as properties. TypeScript allows you to define the exact shape of objects and create reusable type definitions. Let's master object types and type aliases!
Object Types in TypeScript
In TypeScript, you can specify the exact structure an object should have—what properties it contains and what type each property is.
Inline Object Types
// Inline object type annotation
let user: { name: string; age: number; email: string } = {
name: "Yusuf Ibrahim",
age: 25,
email: "yusuf@example.com"
};
// Function with inline object parameter type
function greetUser(user: { name: string; age: number }): string {
return `Hello, ${user.name}! You are ${user.age} years old.`;
}
// Using the function
let greeting = greetUser({ name: "Amina", age: 30 });
console.log(greeting); // "Hello, Amina! You are 30 years old."
// Object with different property types
let product: {
id: number;
name: string;
price: number;
inStock: boolean;
} = {
id: 1,
name: "Laptop",
price: 250000,
inStock: true
};
console.log(`${product.name}: ₦${product.price}`);Inline object types work well for simple, one-time use cases, but they become unwieldy for complex objects or when you need to reuse the same type. That's where type aliases come in!
Type Aliases
Type aliases let you create a reusable name for any type. Use the type keyword to define them.
Basic Type Aliases
// Create a type alias for an object
type User = {
name: string;
age: number;
email: string;
};
// Use the type alias
let user1: User = {
name: "Yusuf Ibrahim",
age: 25,
email: "yusuf@example.com"
};
let user2: User = {
name: "Amina Hassan",
age: 30,
email: "amina@example.com"
};
// Function using type alias
function displayUser(user: User): void {
console.log(`Name: ${user.name}`);
console.log(`Age: ${user.age}`);
console.log(`Email: ${user.email}`);
}
displayUser(user1);
// Type alias for primitive types (less common but possible)
type ID = number;
type Email = string;
let userId: ID = 12345;
let userEmail: Email = "user@example.com";Type Aliases for Complex Objects
// Product type
type Product = {
id: number;
name: string;
description: string;
price: number;
category: string;
inStock: boolean;
tags: string[];
};
// Order type
type Order = {
orderId: number;
customerId: number;
products: Product[];
totalAmount: number;
status: string;
orderDate: Date;
};
// Customer type
type Customer = {
id: number;
name: string;
email: string;
phone: string;
address: {
street: string;
city: string;
state: string;
zipCode: string;
};
};
// Using the types
let laptop: Product = {
id: 1,
name: "Dell XPS 15",
description: "High-performance laptop",
price: 450000,
category: "Electronics",
inStock: true,
tags: ["laptop", "computer", "dell"]
};
let customer: Customer = {
id: 101,
name: "Yusuf Ibrahim",
email: "yusuf@example.com",
phone: "+234-803-123-4567",
address: {
street: "123 Main Street",
city: "Lagos",
state: "Lagos State",
zipCode: "100001"
}
};
console.log(`Customer: ${customer.name} from ${customer.address.city}`);User Type Structure
Complete user profile structure
type User = {
id: number;
name: string;
email: string;
age?: number;
readonly createdAt: Date;
}Unique identifier (required)
User's full name (required)
User's email address (required)
User's age (optional - may not be provided)
Account creation date (cannot be modified)
Optional Properties
Sometimes object properties are optional—they may or may not be present. Use ? to mark properties as optional.
// Type with optional properties
type User = {
id: number;
name: string;
email: string;
age?: number; // Optional
phone?: string; // Optional
address?: string; // Optional
};
// Valid: All required properties present
let user1: User = {
id: 1,
name: "Yusuf Ibrahim",
email: "yusuf@example.com"
};
// Valid: Includes optional properties
let user2: User = {
id: 2,
name: "Amina Hassan",
email: "amina@example.com",
age: 30,
phone: "+234-803-123-4567"
};
// Valid: All properties including optional ones
let user3: User = {
id: 3,
name: "Chidi Okafor",
email: "chidi@example.com",
age: 28,
phone: "+234-805-987-6543",
address: "Lagos, Africa"
};
// Function handling optional properties
function displayUserInfo(user: User): void {
console.log(`Name: ${user.name}`);
console.log(`Email: ${user.email}`);
// Check if optional property exists
if (user.age !== undefined) {
console.log(`Age: ${user.age}`);
}
// Using optional chaining
console.log(`Phone: ${user.phone ?? "Not provided"}`);
console.log(`Address: ${user.address ?? "Not provided"}`);
}
displayUserInfo(user1);
displayUserInfo(user2);💡 Optional vs Undefined
property? is equivalent to property: Type | undefined. Both mean the property may not be present. Use optional properties (?) for better readability.
Default Values with Optional Properties
type Config = {
host: string;
port?: number;
secure?: boolean;
timeout?: number;
};
function createConnection(config: Config): void {
// Provide default values for optional properties
const port = config.port ?? 8080;
const secure = config.secure ?? false;
const timeout = config.timeout ?? 30000;
console.log(`Connecting to ${config.host}:${port}`);
console.log(`Secure: ${secure}`);
console.log(`Timeout: ${timeout}ms`);
}
// Only required property
createConnection({ host: "localhost" });
// Output:
// Connecting to localhost:8080
// Secure: false
// Timeout: 30000ms
// With some optional properties
createConnection({
host: "api.example.com",
port: 443,
secure: true
});
// Output:
// Connecting to api.example.com:443
// Secure: true
// Timeout: 30000msReadonly Properties
Use readonly to prevent properties from being modified after object creation.
// Type with readonly properties
type User = {
readonly id: number; // Cannot be changed
readonly createdAt: Date; // Cannot be changed
name: string; // Can be changed
email: string; // Can be changed
};
let user: User = {
id: 1,
createdAt: new Date(),
name: "Yusuf Ibrahim",
email: "yusuf@example.com"
};
// These are allowed
user.name = "Yusuf A. Ibrahim"; // ✅ Works
user.email = "yusuf.new@example.com"; // ✅ Works
// These cause errors
// user.id = 2; // ❌ Error: Cannot assign to 'id'
// user.createdAt = new Date(); // ❌ Error: Cannot assign to 'createdAt'
console.log(`User ID: ${user.id} (readonly)`);
console.log(`Name: ${user.name} (can be changed)`);Readonly in Nested Objects
type Article = {
readonly id: number;
readonly author: {
readonly name: string;
readonly email: string;
};
title: string;
content: string;
};
let article: Article = {
id: 1,
author: {
name: "Yusuf Ibrahim",
email: "yusuf@example.com"
},
title: "TypeScript Guide",
content: "Learning TypeScript..."
};
// Can modify non-readonly properties
article.title = "Complete TypeScript Guide"; // ✅ Works
article.content = "Updated content..."; // ✅ Works
// Cannot modify readonly properties
// article.id = 2; // ❌ Error
// article.author.name = "New Name"; // ❌ Error
// article.author.email = "new@example.com"; // ❌ Error
// Can reassign the entire object
article = {
id: 2, // Different ID allowed when creating new object
author: {
name: "Amina Hassan",
email: "amina@example.com"
},
title: "New Article",
content: "New content..."
};🔒 When to Use Readonly
- Database IDs that should never change
- Timestamps (createdAt, updatedAt)
- Configuration values that are set once
- Any property that represents immutable data
Nested Objects
Objects can contain other objects as properties, creating nested structures.
// Type with nested objects
type Address = {
street: string;
city: string;
state: string;
country: string;
zipCode: string;
};
type Company = {
name: string;
industry: string;
founded: number;
};
type Employee = {
id: number;
name: string;
email: string;
address: Address; // Nested object
company: Company; // Nested object
salary: number;
};
// Creating an object with nested structure
let employee: Employee = {
id: 1,
name: "Yusuf Ibrahim",
email: "yusuf@company.com",
address: {
street: "123 Main Street",
city: "Lagos",
state: "Lagos State",
country: "Nigeria",
zipCode: "100001"
},
company: {
name: "TechCorp Africa",
industry: "Technology",
founded: 2015
},
salary: 500000
};
// Accessing nested properties
console.log(`Employee: ${employee.name}`);
console.log(`City: ${employee.address.city}`);
console.log(`Company: ${employee.company.name}`);
console.log(`Founded: ${employee.company.founded}`);
// Function working with nested objects
function displayEmployeeLocation(emp: Employee): string {
return `${emp.name} works in ${emp.address.city}, ${emp.address.country}`;
}
console.log(displayEmployeeLocation(employee));Inline vs Separate Type Definitions
// Option 1: Inline nested object type
type Product1 = {
id: number;
name: string;
vendor: {
name: string;
contact: string;
};
};
// Option 2: Separate type definitions (better for reusability)
type Vendor = {
name: string;
contact: string;
};
type Product2 = {
id: number;
name: string;
vendor: Vendor; // Reusable type
};
// Now Vendor type can be used elsewhere
let vendor1: Vendor = {
name: "TechSupplies Ltd",
contact: "tech@supplies.com"
};
let vendor2: Vendor = {
name: "ElectroMart",
contact: "info@electromart.com"
};
let product: Product2 = {
id: 1,
name: "Laptop",
vendor: vendor1
};Objects with Methods
Objects can have methods (functions as properties).
// Type with method signatures
type Calculator = {
value: number;
add: (n: number) => number;
subtract: (n: number) => number;
multiply: (n: number) => number;
divide: (n: number) => number;
reset: () => void;
};
// Implementing the object
let calculator: Calculator = {
value: 0,
add(n: number): number {
this.value += n;
return this.value;
},
subtract(n: number): number {
this.value -= n;
return this.value;
},
multiply(n: number): number {
this.value *= n;
return this.value;
},
divide(n: number): number {
if (n === 0) {
throw new Error("Cannot divide by zero");
}
this.value /= n;
return this.value;
},
reset(): void {
this.value = 0;
}
};
// Using the calculator
calculator.add(10); // 10
calculator.multiply(5); // 50
calculator.subtract(20); // 30
console.log(`Result: ${calculator.value}`); // 30
calculator.reset();
console.log(`After reset: ${calculator.value}`); // 0Alternative Method Syntax
// Type with different method syntax options
type Person = {
name: string;
// Method syntax 1: Property with function type
greet1: (greeting: string) => string;
// Method syntax 2: Method signature (preferred for object types)
greet2(greeting: string): string;
};
let person: Person = {
name: "Yusuf",
greet1: function(greeting: string): string {
return `${greeting}, I'm ${this.name}`;
},
greet2(greeting: string): string {
return `${greeting}, I'm ${this.name}`;
}
};
console.log(person.greet1("Hello")); // "Hello, I'm Yusuf"
console.log(person.greet2("Hi")); // "Hi, I'm Yusuf"Index Signatures
Index signatures allow objects to have properties with dynamic names of a certain type.
// Index signature: properties can have any string key
type Dictionary = {
[key: string]: string;
};
let translations: Dictionary = {
hello: "Sannu",
goodbye: "Sai anjima",
thank_you: "Na gode"
};
// Can add any string key
translations.yes = "Eh";
translations.no = "A'a";
console.log(translations.hello); // "Sannu"
console.log(translations.thank_you); // "Na gode"
// Index signature with number keys
type NumberDictionary = {
[index: number]: string;
};
let days: NumberDictionary = {
0: "Sunday",
1: "Monday",
2: "Tuesday",
3: "Wednesday",
4: "Thursday",
5: "Friday",
6: "Saturday"
};
console.log(days[0]); // "Sunday"
console.log(days[5]); // "Friday"Combining Index Signatures with Known Properties
// Type with specific properties AND index signature
type Config = {
host: string; // Required specific property
port: number; // Required specific property
[key: string]: string | number; // Additional dynamic properties
};
let serverConfig: Config = {
host: "localhost",
port: 8080,
timeout: 30000, // Additional property (number)
maxConnections: 100, // Additional property (number)
environment: "development" // Additional property (string)
};
console.log(serverConfig.host); // "localhost"
console.log(serverConfig.timeout); // 30000
console.log(serverConfig.environment); // "development"
// Practical example: API Response
type ApiResponse = {
status: number;
message: string;
[key: string]: any; // Allow any additional data
};
let response: ApiResponse = {
status: 200,
message: "Success",
data: { id: 1, name: "User" },
timestamp: new Date(),
requestId: "abc-123"
};Type Aliases vs Interfaces Preview
While we'll cover interfaces in detail in a later lesson, it's worth noting that type aliases and interfaces are similar for object types.
// Using Type Alias
type UserType = {
id: number;
name: string;
email: string;
};
// Using Interface (covered in detail later)
interface UserInterface {
id: number;
name: string;
email: string;
}
// Both work similarly for objects
let user1: UserType = {
id: 1,
name: "Yusuf",
email: "yusuf@example.com"
};
let user2: UserInterface = {
id: 2,
name: "Amina",
email: "amina@example.com"
};
// Type aliases can do more than interfaces
type ID = number | string; // Union types
type Callback = () => void; // Function types
type StringArray = string[]; // Array types
// We'll learn when to use each in the Interfaces lesson!For now, use type aliases for object types. We'll explore interfaces in depth in a future lesson and learn when to choose one over the other.
Practical Example: E-Commerce System
Let's build a complete type system for an e-commerce application:
// Base types
type Product = {
readonly id: number;
name: string;
description: string;
price: number;
category: string;
inStock: boolean;
images: string[];
rating?: number;
};
type CartItem = {
product: Product;
quantity: number;
};
type Address = {
street: string;
city: string;
state: string;
zipCode: string;
};
type Customer = {
readonly id: number;
name: string;
email: string;
phone?: string;
addresses: Address[];
readonly registeredAt: Date;
};
type Order = {
readonly id: number;
readonly orderId: string;
customer: Customer;
items: CartItem[];
shippingAddress: Address;
totalAmount: number;
status: "pending" | "processing" | "shipped" | "delivered";
readonly createdAt: Date;
deliveredAt?: Date;
};
// Sample data
const laptop: Product = {
id: 1,
name: "Dell XPS 15",
description: "High-performance laptop for professionals",
price: 450000,
category: "Electronics",
inStock: true,
images: ["laptop1.jpg", "laptop2.jpg"],
rating: 4.5
};
const customer: Customer = {
id: 101,
name: "Yusuf Ibrahim",
email: "yusuf@example.com",
phone: "+234-803-123-4567",
addresses: [
{
street: "123 Main Street",
city: "Lagos",
state: "Lagos State",
zipCode: "100001"
}
],
registeredAt: new Date("2024-01-15")
};
const order: Order = {
id: 1001,
orderId: "ORD-2024-1001",
customer: customer,
items: [
{
product: laptop,
quantity: 1
}
],
shippingAddress: customer.addresses[0],
totalAmount: 450000,
status: "pending",
createdAt: new Date()
};
// Helper functions
function calculateOrderTotal(items: CartItem[]): number {
return items.reduce((total, item) => {
return total + (item.product.price * item.quantity);
}, 0);
}
function displayOrderSummary(order: Order): void {
console.log(`Order ID: ${order.orderId}`);
console.log(`Customer: ${order.customer.name}`);
console.log(`Items:`);
order.items.forEach(item => {
console.log(` - ${item.product.name} x ${item.quantity} = ₦${item.product.price * item.quantity}`);
});
console.log(`Total: ₦${order.totalAmount}`);
console.log(`Status: ${order.status}`);
console.log(`Shipping to: ${order.shippingAddress.city}, ${order.shippingAddress.state}`);
}
displayOrderSummary(order);Test Your Knowledge
Type Safety Check
Which object assignment is valid?
type User = {
name: string;
age: number;
email: string;
};
// Option A
const user1: User = { name: "Yusuf", age: 25 };
// Option B
const user2: User = { name: "Amina", age: 30, email: "amina@example.com" };
// Option C
const user3: User = { name: "Chidi", age: "28", email: "chidi@example.com" };
// Option D
const user4: User = { name: "Fatima", age: 22, email: "fatima@example.com", city: "Lagos" };Common TypeScript Error
// Attempting to access non-existent property
type Product = {
id: number;
name: string;
price: number;
};
const laptop: Product = {
id: 1,
name: "Dell Laptop",
price: 250000
};
// Trying to access a property that doesn't exist
console.log(laptop.category); // Error!
laptop.discount = 10; // Error!❌ Property 'category' does not exist on type 'Product'. Property 'discount' does not exist on type 'Product'.
What's Wrong?
TypeScript only allows you to access properties that are defined in the object's type. This prevents typos and ensures you're working with the expected structure.
// Fix 1: Add property to type
type Product = {
id: number;
name: string;
price: number;
category?: string; // Optional property
discount?: number; // Optional property
};
const laptop: Product = {
id: 1,
name: "Dell Laptop",
price: 250000,
category: "Electronics",
discount: 10
};
// Fix 2: Use existing properties
console.log(laptop.name); // ✅ Works
console.log(laptop.price); // ✅ WorksSolution: Add the property to the type definition or use the correct property name
Best Practices
- Use type aliases for reusable object types
- Mark properties as readonly when they shouldn't change
- Use optional properties (
?) for properties that may not exist - Extract nested types into separate type aliases for reusability
- Use descriptive names for type aliases (PascalCase convention)
- Keep types close to where they're used in the code
- Use index signatures sparingly—prefer known properties when possible
- Document complex types with comments
// ✅ Good: Reusable, well-structured types
// Base types
type UUID = string;
type Timestamp = Date;
// Shared types
type Address = {
street: string;
city: string;
state: string;
zipCode: string;
};
// Main type with readonly and optional properties
type User = {
readonly id: UUID;
readonly createdAt: Timestamp;
name: string;
email: string;
age?: number; // Optional
phone?: string; // Optional
address?: Address; // Optional nested type
};
// Function with clear type usage
function createUser(
name: string,
email: string,
address?: Address
): User {
return {
id: crypto.randomUUID(),
createdAt: new Date(),
name,
email,
address
};
}
// ❌ Avoid: Inline types, hard to reuse
function badExample(
user: {
id: string;
name: string;
email: string;
address: {
street: string;
city: string;
};
}
): void {
// Inline type is hard to reuse and maintain
}Key Takeaways
- Objects in TypeScript have defined shapes with specific property types
- Type aliases create reusable names for types using the
typekeyword - Optional properties use
?and may be undefined - Readonly properties use
readonlyand cannot be modified - Objects can be nested to create complex data structures
- Methods can be defined as properties with function types
- Index signatures allow dynamic property names with specific value types
- Type aliases work for objects, primitives, unions, and more
- Extract nested types for better reusability and maintainability
- Use PascalCase for type alias names (convention)
What's Next?
Now that you understand objects and type aliases, you're ready to explore Enums! In the next lesson, you'll learn how to create named constants with numeric and string enums, understand when to use them, and see how they improve code readability. Enums are perfect for representing a fixed set of related values.
Get ready to organize your constants with TypeScript enums!