In the previous tutorial, we learned how to type functions. Now let’s learn how to describe the shape of objects — one of the most important skills in TypeScript.
By the end of this tutorial, you will know how to define object types, use interfaces, extend them, and choose between interfaces and type aliases.
Object Types
You can describe an object’s shape by listing its properties and types:
let user: { name: string; age: number } = {
name: "Alex",
age: 25,
};
TypeScript checks that the object matches the shape:
let user: { name: string; age: number } = {
name: "Alex",
// Error: Property 'age' is missing in type '{ name: string; }'
};
You can use object types in function parameters:
function printUser(user: { name: string; age: number }): void {
console.log(`${user.name} is ${user.age} years old`);
}
printUser({ name: "Alex", age: 25 }); // OK
printUser({ name: "Alex" }); // Error: 'age' is missing
Writing { name: string; age: number } everywhere gets repetitive. That is where interfaces come in.
Interfaces
An interface defines a reusable shape for objects:
interface User {
name: string;
age: number;
email: string;
}
const user: User = {
name: "Alex",
age: 25,
email: "alex@example.com",
};
Now you can use User anywhere instead of writing out the full object type:
function printUser(user: User): void {
console.log(`${user.name} (${user.email})`);
}
function getUsers(): User[] {
return [
{ name: "Alex", age: 25, email: "alex@example.com" },
{ name: "Sam", age: 30, email: "sam@example.com" },
];
}
Interfaces make your code cleaner and easier to maintain. Change the interface once, and every function that uses it gets updated.
Optional Properties
Add ? after a property name to make it optional:
interface User {
name: string;
age: number;
email?: string; // optional
}
const user1: User = { name: "Alex", age: 25 }; // OK
const user2: User = { name: "Sam", age: 30, email: "sam@example.com" }; // OK
Optional properties have undefined as a possible value. When you access them, TypeScript knows they might not exist:
function sendEmail(user: User): void {
if (user.email) {
console.log(`Sending email to ${user.email}`);
} else {
console.log("No email address");
}
}
Readonly Properties
Use readonly to prevent a property from being changed after creation:
interface User {
readonly id: number;
name: string;
age: number;
}
const user: User = { id: 1, name: "Alex", age: 25 };
user.name = "Sam"; // OK — name is not readonly
user.id = 2; // Error: Cannot assign to 'id' because it is a read-only property
Use readonly for IDs, creation dates, and other values that should never change.
Note: readonly is a compile-time check only. At runtime, the property can still be changed. TypeScript trusts you to follow the rules.
Extending Interfaces
You can build new interfaces from existing ones with extends:
interface User {
name: string;
age: number;
email: string;
}
interface Admin extends User {
role: string;
permissions: string[];
}
const admin: Admin = {
name: "Alex",
age: 25,
email: "alex@example.com",
role: "admin",
permissions: ["read", "write", "delete"],
};
Admin has everything from User plus role and permissions.
You can extend multiple interfaces:
interface HasName {
name: string;
}
interface HasEmail {
email: string;
}
interface Contact extends HasName, HasEmail {
phone: string;
}
const contact: Contact = {
name: "Alex",
email: "alex@example.com",
phone: "+49-123-456",
};
This is great for composing small, focused interfaces into bigger ones.
Interface vs Type Alias
TypeScript has two ways to name an object shape: interface and type. They are very similar:
// Interface
interface User {
name: string;
age: number;
}
// Type alias
type UserType = {
name: string;
age: number;
};
Both work in the same places. But there are differences:
| Feature | interface | type |
|---|---|---|
| Extend | extends keyword | & intersection |
| Merge (declaration merging) | Yes | No |
| Union types | No | Yes |
| Primitive types | No | Yes |
| Mapped types | No | Yes |
When to Use Each
Use interface when:
- Defining the shape of objects
- You want to extend the shape later
- You’re working with classes
Use type when:
- Defining union types:
type Status = "active" \| "inactive" - Defining tuple types:
type Pair = [string, number] - Combining types with intersections
Simple rule: Use interface for objects, type for everything else. When in doubt, use interface.
Declaration Merging
One unique feature of interface is declaration merging. If you declare the same interface twice, TypeScript combines them:
interface User {
name: string;
}
interface User {
age: number;
}
// User now has both name and age
const user: User = {
name: "Alex",
age: 25,
};
This is useful when you need to extend a third-party library’s types. But it can also cause confusion, so use it intentionally.
type does not support declaration merging. Declaring the same type twice is an error.
Index Signatures
Sometimes you don’t know all property names in advance. Use an index signature:
interface Dictionary {
[key: string]: string;
}
const translations: Dictionary = {
hello: "hola",
goodbye: "adios",
thanks: "gracias",
};
// You can add any string key
translations["please"] = "por favor";
The [key: string]: string means “any string key maps to a string value.”
You can combine index signatures with known properties:
interface Config {
name: string;
version: number;
[key: string]: string | number;
}
const config: Config = {
name: "my-app",
version: 1,
port: 3000,
host: "localhost",
};
Note: When you have an index signature, all known properties must have types compatible with the index signature type.
Excess Property Checking
TypeScript has a special rule: when you assign an object literal directly to a typed variable, it checks for extra properties:
interface User {
name: string;
age: number;
}
// Error: Object literal may only specify known properties, and 'email' does not exist in type 'User'
const user: User = {
name: "Alex",
age: 25,
email: "alex@example.com", // Extra property — error!
};
This catches typos and accidental extra fields. But it only applies to object literals assigned directly. If you pass through a variable, the check does not apply:
const data = {
name: "Alex",
age: 25,
email: "alex@example.com",
};
const user: User = data; // OK — no excess property checking
This works because of duck typing (structural typing).
Duck Typing (Structural Typing)
TypeScript uses structural typing. If an object has all the required properties, it fits the type — even if it has extra properties:
interface HasName {
name: string;
}
function greet(thing: HasName): string {
return `Hello, ${thing.name}!`;
}
const user = { name: "Alex", age: 25, email: "alex@example.com" };
greet(user); // OK — user has 'name', that's enough
TypeScript does not care that user has age and email. It only checks that user has name: string.
This is called “duck typing”: if it looks like a duck and quacks like a duck, it is a duck.
Nested Objects
Interfaces can describe nested structures:
interface Address {
street: string;
city: string;
country: string;
}
interface User {
name: string;
age: number;
address: Address;
}
const user: User = {
name: "Alex",
age: 25,
address: {
street: "123 Main St",
city: "Berlin",
country: "Germany",
},
};
Break complex objects into smaller interfaces. It makes your code more readable and reusable.
Quick Practice
Create src/objects.ts and try these:
// Define interfaces
interface Product {
readonly id: number;
name: string;
price: number;
description?: string;
}
interface CartItem extends Product {
quantity: number;
}
// Create objects
const laptop: Product = {
id: 1,
name: "Laptop",
price: 999.99,
description: "A fast laptop for developers",
};
const cartItem: CartItem = {
...laptop,
quantity: 2,
};
// Function using interfaces
function getTotal(items: CartItem[]): number {
let total = 0;
for (const item of items) {
total += item.price * item.quantity;
}
return total;
}
// Index signature example
interface Inventory {
[productName: string]: number;
}
const stock: Inventory = {
laptop: 50,
keyboard: 200,
mouse: 150,
};
// Test it
console.log(`Cart item: ${cartItem.name} x${cartItem.quantity}`);
console.log(`Total: $${getTotal([cartItem])}`);
console.log(`Laptops in stock: ${stock["laptop"]}`);
Run it:
npx tsx src/objects.ts
Output:
Cart item: Laptop x2
Total: $1999.98
Laptops in stock: 50
What is Next?
In the next tutorial, we will learn about union types, literal types, and type aliases — the patterns that make TypeScript truly powerful.
Related Articles
- TypeScript Tutorial #4: Functions and Type Annotations — previous tutorial in this series
- Go Tutorial: Goroutines — how Go structures code differently