Skip to main content

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
info

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
};
warning

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:

  1. Define a Product type

    • id: number (read-only)
    • name: string
    • price: number
    • description: string (optional)
    • category: 'electronics' | 'clothing' | 'food' | 'other'
    • inStock: boolean
    • tags: an array of strings (optional)
  2. Define a CartItem type

    • product: the Product type
    • quantity: number
  3. Create a calculateTotal function

    • It takes an array of CartItem and returns the total price
Hint
  1. Define the Product type with type Product = { ... }
  2. Add the readonly modifier to id
  3. Add ? to optional properties
  4. Calculate the total in the calculateTotal function with the reduce method
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 Product type, the readonly modifier makes id unchangeable
  • description and tags are made optional with ?
  • Defining ProductCategory as a separate type improves reusability
  • The calculateTotal function aggregates the array with the reduce method 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

warning

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.


Continue to Special types. You learn the behavior of TypeScript's special types, such as any and unknown, and when to use each.