Object types and type aliases
In this chapter, you learn how to define object types and how to create reusable types with type aliases (type).
What you learn in this chapter
- How to define object types
- How to use type aliases (type)
- Optional properties (?)
- The readonly modifier
- Index signatures
The content of this chapter covers important concepts you use frequently in real application development. Defining object types properly lets you write type-safe code.
Object type basics
An object type defines properties and their types.
Type annotations for objects
// A type annotation for an object
let person: {
name: string;
age: number;
} = {
name: 'Alice',
age: 25
};
// Accessing properties
console.log(person.name); // 'Alice'
console.log(person.age); // 25
// Changing a property
person.age = 26; // OK
// Accessing a property that does not exist is an error
// person.email = 'alice@example.com'; // Error: property 'email' does not exist on the type
Using type inference
// Leave it to type inference (recommended)
let person = {
name: 'Alice',
age: 25
};
// The type of person: { name: string; age: number; }
// Even after the type is inferred, you cannot add properties
// person.email = 'alice@example.com'; // Error
Type aliases
A feature that gives a name to a complex type so it can be reused.
A basic type alias
// Basic syntax: type TypeName = Type;
type UserID = number;
type UserName = string;
let id: UserID = 123;
let name: UserName = 'Alice';
A type alias for an object type
// Give a name to an object's type
type User = {
id: number;
name: string;
email: string;
age: number;
};
let user1: User = {
id: 1,
name: 'Alice',
email: 'alice@example.com',
age: 25
};
let user2: User = {
id: 2,
name: 'Bob',
email: 'bob@example.com',
age: 30
};
A type alias for a union type
// Give a name to a union type you use often
type Status = 'pending' | 'approved' | 'rejected';
type ID = string | number;
let orderStatus: Status = 'pending';
let userId: ID = 123;
let productId: ID = 'ABC-001';
A type alias for a complex type
// Combine complex object types
type Address = {
zipCode: string;
prefecture: string;
city: string;
street: string;
building?: string; // optional
};
type Contact = {
email: string;
phone?: string; // optional
};
type Customer = {
id: number;
name: string;
address: Address;
contact: Contact;
registeredAt: Date;
};
let customer: Customer = {
id: 1,
name: 'Taro Tanaka',
address: {
zipCode: '100-0001',
prefecture: 'Tokyo',
city: 'Chiyoda',
street: '1-1-1 Chiyoda'
},
contact: {
email: 'tanaka@example.com',
phone: '03-1234-5678'
},
registeredAt: new Date()
};
A type alias for a function type
// Give an alias to a function's type
type MathOperation = (a: number, b: number) => number;
const add: MathOperation = (a, b) => a + b;
const subtract: MathOperation = (a, b) => a - b;
const multiply: MathOperation = (a, b) => a * b;
const divide: MathOperation = (a, b) => a / b;
console.log(add(10, 5)); // 15
console.log(subtract(10, 5)); // 5
console.log(multiply(10, 5)); // 50
console.log(divide(10, 5)); // 2
// A type for a callback function
type Callback = (result: string) => void;
type Validator = (value: string) => boolean;
Optional properties
Adding ? after a property name makes that property optional.
// Optional property (add ?)
type User = {
name: string;
age: number;
email?: string; // can be omitted
};
// Omit email
let user1: User = {
name: 'Bob',
age: 30
};
// Specify email
let user2: User = {
name: 'Charlie',
age: 35,
email: 'charlie@example.com'
};
// Accessing an optional property
console.log(user1.email); // undefined
console.log(user2.email); // 'charlie@example.com'
Using optional properties safely
type User = {
name: string;
email?: string;
};
let user: User = { name: 'Alice' };
// When you use an optional property, you need an existence check
if (user.email) {
console.log(user.email.toUpperCase()); // OK
}
// Access safely with optional chaining (the ?. operator)
console.log(user.email?.toUpperCase()); // undefined (no error)
// Set a default value with nullish coalescing (the ?? operator)
let email = user.email ?? 'no-email@example.com';
console.log(email); // 'no-email@example.com'
The readonly modifier
Adding readonly makes a property read-only.
// readonly properties
type Config = {
readonly apiUrl: string;
readonly timeout: number;
retries: number; // a normal property
};
let config: Config = {
apiUrl: 'https://api.example.com',
timeout: 5000,
retries: 3
};
// Reading is OK
console.log(config.apiUrl); // 'https://api.example.com'
console.log(config.timeout); // 5000
// Normal properties can be changed
config.retries = 5; // OK
// readonly properties cannot be changed
// config.apiUrl = 'https://api2.example.com'; // Error
// config.timeout = 3000; // Error
A practical use of readonly
// An API response type (immutable)
type ApiResponse = {
readonly status: number;
readonly data: {
readonly id: number;
readonly name: string;
};
};
// A constants object
type AppConstants = {
readonly APP_NAME: string;
readonly VERSION: string;
readonly MAX_RETRIES: number;
};
const APP_CONSTANTS: AppConstants = {
APP_NAME: 'MyApp',
VERSION: '1.0.0',
MAX_RETRIES: 3
};
// APP_CONSTANTS.APP_NAME = 'NewApp'; // Error
The Readonly<T> utility type
To make all properties readonly at once, use Readonly<T>.
type User = {
id: number;
name: string;
email: string;
};
// All properties become readonly
type ReadonlyUser = Readonly<User>;
// {
// readonly id: number;
// readonly name: string;
// readonly email: string;
// }
let user: ReadonlyUser = {
id: 1,
name: 'Alice',
email: 'alice@example.com'
};
// user.name = 'Bob'; // Error: readonly
Index signatures
Use these when property names are dynamic, or when there are many properties.
A basic index signature
// Basic syntax of an index signature
type Dictionary = {
[key: string]: string;
};
let dictionary: Dictionary = {
apple: 'red',
banana: 'yellow',
orange: 'orange'
};
// You can access it with any key
console.log(dictionary.apple); // 'red'
console.log(dictionary['banana']); // 'yellow'
// Add new properties
dictionary.grape = 'purple'; // OK
dictionary['melon'] = 'green'; // OK
Practical examples
// Example 1: a scoreboard
type Scores = {
[playerName: string]: number;
};
let scores: Scores = {
'Alice': 100,
'Bob': 85,
'Charlie': 92
};
scores['David'] = 88; // OK
// Example 2: a settings object
type Settings = {
[key: string]: string | number | boolean;
};
let settings: Settings = {
theme: 'dark',
fontSize: 14,
autoSave: true,
language: 'ja'
};
// Example 3: API response headers
type Headers = {
[headerName: string]: string;
};
let headers: Headers = {
'Content-Type': 'application/json',
'Authorization': 'Bearer token123',
'Accept-Language': 'ja-JP'
};
Combining an index signature with fixed properties
// Combine fixed properties with an index signature
type User = {
id: number; // a required property
name: string; // a required property
[key: string]: string | number; // any other properties
};
let user: User = {
id: 1,
name: 'Alice',
email: 'alice@example.com', // OK (allowed by the index signature)
age: 25 // OK
};
The type of the index signature and the types of the fixed properties must be compatible.
// An example that errors
type Invalid = {
name: string;
[key: string]: number; // Error: name is a string but the index signature is a number
};
// Fix: use a union type
type Valid = {
name: string;
[key: string]: string | number; // OK
};
Combining type aliases
Intersection types
Compose multiple types to create a type that has all the properties.
// Combine existing type aliases
type Point = {
x: number;
y: number;
};
type Color = {
r: number;
g: number;
b: number;
};
// Combine with an intersection type
type ColoredPoint = Point & Color;
let point: ColoredPoint = {
x: 10,
y: 20,
r: 255,
g: 0,
b: 0
};
Practical type design
// A basic entity type
type BaseEntity = {
id: number;
createdAt: Date;
updatedAt: Date;
};
// User-specific properties
type UserData = {
name: string;
email: string;
role: 'admin' | 'user' | 'guest';
};
// Combine to create the User type
type User = BaseEntity & UserData;
let user: User = {
id: 1,
createdAt: new Date(),
updatedAt: new Date(),
name: 'Alice',
email: 'alice@example.com',
role: 'admin'
};
// Create a Product type the same way
type ProductData = {
name: string;
price: number;
stock: number;
};
type Product = BaseEntity & ProductData;
Try it: design a type for product data ★★
Design a type that represents product data for an e-commerce site.
Requirements:
-
Define a
Producttype- id: number (read-only)
- name: string
- price: number
- description: string (optional)
- category: 'electronics' | 'clothing' | 'food' | 'other'
- inStock: boolean
- tags: an array of strings (optional)
-
Define a
CartItemtype- product: the Product type
- quantity: number
-
Create a
calculateTotalfunction- It takes an array of CartItem and returns the total price
Hint
- Define the
Producttype withtype Product = { ... } - Add the
readonlymodifier to id - Add
?to optional properties - Calculate the total in the
calculateTotalfunction with thereducemethod
Answer and explanation
// The product category type
type ProductCategory = 'electronics' | 'clothing' | 'food' | 'other';
// The product type definition
type Product = {
readonly id: number; // read-only
name: string;
price: number;
description?: string; // optional
category: ProductCategory;
inStock: boolean;
tags?: string[]; // optional
};
// The cart item type definition
type CartItem = {
product: Product;
quantity: number;
};
// A function that calculates the total price
function calculateTotal(items: CartItem[]): number {
return items.reduce((total, item) => {
return total + (item.product.price * item.quantity);
}, 0);
}
// Test data
const laptop: Product = {
id: 1,
name: 'Laptop',
price: 89800,
description: 'High-performance laptop',
category: 'electronics',
inStock: true,
tags: ['PC', 'Business']
};
const tshirt: Product = {
id: 2,
name: 'T-shirt',
price: 2980,
category: 'clothing',
inStock: true
// description and tags can be omitted
};
const snack: Product = {
id: 3,
name: 'Potato chips',
price: 198,
category: 'food',
inStock: false
};
// Cart items
const cartItems: CartItem[] = [
{ product: laptop, quantity: 1 },
{ product: tshirt, quantity: 2 },
{ product: snack, quantity: 5 }
];
// Calculate the total price
const total = calculateTotal(cartItems);
console.log(`Total: ${total.toLocaleString()}`);
// Output: Total: 96,750
// (89800 * 1) + (2980 * 2) + (198 * 5) = 89800 + 5960 + 990 = 96750
// Display product information
function displayProduct(product: Product): void {
console.log(`Product: ${product.name}`);
console.log(`Price: ${product.price.toLocaleString()}`);
console.log(`Category: ${product.category}`);
console.log(`Stock: ${product.inStock ? 'In stock' : 'Out of stock'}`);
// Existence check for optional properties
if (product.description) {
console.log(`Description: ${product.description}`);
}
if (product.tags && product.tags.length > 0) {
console.log(`Tags: ${product.tags.join(', ')}`);
}
console.log('---');
}
displayProduct(laptop);
displayProduct(tshirt);
displayProduct(snack);
Explanation:
- In the
Producttype, thereadonlymodifier makes id unchangeable descriptionandtagsare made optional with?- Defining
ProductCategoryas a separate type improves reusability - The
calculateTotalfunction aggregates the array with thereducemethod to calculate the total price - When you use optional properties, use an existence check or optional chaining (
?.)
An example of extending the type:
// A type with discount information added
type DiscountedProduct = Product & {
discountRate: number; // discount rate (0.1 = 10%)
originalPrice: number;
};
// A sale item
const saleItem: DiscountedProduct = {
id: 4,
name: 'Sale item',
price: 8980, // price after discount
originalPrice: 9980,
discountRate: 0.1,
category: 'electronics',
inStock: true
};
Summary
What you learned in this chapter:
- Object types: define properties and their types
- Type aliases (type): give a name to a complex type for reuse
- Optional properties (?): define properties that can be omitted
- The readonly modifier: define properties that cannot be changed
- Index signatures: handle dynamic property names
- Intersection types (&): combine multiple types
Key points:
- Use type aliases to define reusable types
- Use optional and required properties appropriately
- Use readonly to protect data that should not be changed
- Build complex types by combining small types
In the next chapter, you will learn about special types such as any and unknown.
Common pitfalls for beginners
Common mistakes
❌ Forgetting the existence check for an optional property
type User = {
name: string;
email?: string;
};
const user: User = { name: 'Alice' };
// ❌ Wrong: not considering the possibility of undefined
console.log(user.email.toUpperCase());
// Error: 'user.email' is possibly 'undefined'
// ✅ Correct: use an existence check or optional chaining
console.log(user.email?.toUpperCase()); // undefined (no error)
if (user.email) {
console.log(user.email.toUpperCase()); // OK
}
Cause: An optional property (with ?) may be undefined.
Solution: Use optional chaining (?.) or a conditional to do an existence check.
❌ Trying to change a readonly property
type Config = {
readonly apiUrl: string;
timeout: number;
};
const config: Config = {
apiUrl: 'https://api.example.com',
timeout: 5000
};
// ❌ Wrong: trying to change a readonly property
config.apiUrl = 'https://new-api.example.com';
// Error: Cannot assign to 'apiUrl' because it is a read-only property
// ✅ Correct: create a new object
const newConfig: Config = {
...config,
apiUrl: 'https://new-api.example.com' // OK: building a new object with spread
};
// readonly only forbids reassigning an existing property. It does not prevent building a new object
Cause: The readonly modifier forbids changes.
Solution: When you need a change, create a new object or remove readonly.
❌ A type mismatch between an index signature and fixed properties
// ❌ Wrong: the fixed property type is not compatible with the index signature
type Invalid = {
name: string; // string
[key: string]: number; // Error: 'name' must be a number
};
// ✅ Correct: allow both types with a union type
type Valid = {
name: string;
[key: string]: string | number; // a union type that includes name's type (string)
};
Cause: An index signature must be compatible with the types of all properties on that object.
Solution: Make the index signature a union type that includes the types of the fixed properties.
❌ Confusing a type alias with a value
// A type alias (a type definition)
type User = {
name: string;
age: number;
};
// ❌ Wrong: trying to use a type alias as a value
console.log(User); // Error: 'User' only refers to a type
// ✅ Correct: use a type alias as a type annotation
const user: User = { name: 'Alice', age: 25 };
console.log(user); // OK
Cause: A type alias exists only at compile time and disappears at runtime.
Solution: Use a type alias as a variable's type annotation.
❌ A property conflict in an intersection type
type A = {
value: string;
};
type B = {
value: number;
};
// A property conflict in an intersection type
type AB = A & B;
// The type of value is string & number = never (effectively unusable)
// ❌ Wrong: you cannot assign a value to a property that became never
const ab: AB = {
value: 'hello' // Error: string cannot be assigned to never
};
// ✅ Correct: use property names that do not conflict
type A2 = { stringValue: string };
type B2 = { numberValue: number };
type AB2 = A2 & B2;
const ab2: AB2 = {
stringValue: 'hello',
numberValue: 42
};
Cause: When you intersect types that have the same property name with different types, the result is the never type.
Solution: Avoid property-name conflicts, or revisit your design.
What to read next
Continue to Special types. You learn the behavior of TypeScript's special types, such as any and unknown, and when to use each.