TypeScript

Object Types

26 min Lesson 9 of 40

Object Types

Object types are fundamental to TypeScript, allowing you to describe the shape and structure of objects in your code. Understanding how to properly type objects is essential for building type-safe applications. TypeScript provides various ways to define and work with object types, from simple property annotations to advanced features like index signatures and readonly properties.

Object Type Annotation

The simplest way to type an object is by using object type annotation inline:

Basic Object Type Annotation:

// Inline object type
let user: {
  name: string;
  age: number;
  email: string;
} = {
  name: 'Alice',
  age: 30,
  email: 'alice@example.com'
};

// Function parameter with object type
function displayUser(user: {
  name: string;
  age: number;
  isActive: boolean;
}): void {
  console.log(`User: ${user.name}, Age: ${user.age}`);
  console.log(`Status: ${user.isActive ? 'Active' : 'Inactive'}`);
}

displayUser({
  name: 'Bob',
  age: 25,
  isActive: true
});

// Function returning object type
function createProduct(
  id: string,
  name: string,
  price: number
): {
  id: string;
  name: string;
  price: number;
  createdAt: Date;
} {
  return {
    id,
    name,
    price,
    createdAt: new Date()
  };
}

const product = createProduct('prod-001', 'Laptop', 999);
console.log(product.createdAt); // TypeScript knows this is a Date
Note: While inline object types work for simple cases, it's better to use type aliases or interfaces for reusable object shapes to avoid code duplication.

Optional Properties

Optional properties allow certain properties to be present or absent in an object. Mark properties as optional using the question mark (?):

Optional Properties:

// Type with optional properties
type UserProfile = {
  username: string;
  email: string;
  bio?: string;           // Optional
  website?: string;       // Optional
  phone?: string;         // Optional
};

// Valid: includes all required properties
const user1: UserProfile = {
  username: 'john_doe',
  email: 'john@example.com'
};

// Valid: includes optional properties
const user2: UserProfile = {
  username: 'jane_smith',
  email: 'jane@example.com',
  bio: 'Software developer',
  website: 'https://janesmith.dev'
};

// Function handling optional properties
function displayProfile(profile: UserProfile): void {
  console.log(`Username: ${profile.username}`);
  console.log(`Email: ${profile.email}`);

  // Check if optional property exists
  if (profile.bio) {
    console.log(`Bio: ${profile.bio}`);
  }

  // Optional chaining with optional properties
  console.log(`Website: ${profile.website ?? 'Not provided'}`);
}

// Nested optional properties
type Address = {
  street: string;
  city: string;
  state: string;
  zipCode: string;
  country?: string;
};

type Customer = {
  name: string;
  email: string;
  address?: Address;  // Entire address is optional
};

const customer1: Customer = {
  name: 'Alice',
  email: 'alice@example.com'
};

const customer2: Customer = {
  name: 'Bob',
  email: 'bob@example.com',
  address: {
    street: '123 Main St',
    city: 'New York',
    state: 'NY',
    zipCode: '10001',
    country: 'USA'
  }
};
Tip: Use optional chaining (?.) and nullish coalescing (??) operators to safely access and provide defaults for optional properties.

Readonly Properties

Readonly properties can only be assigned during initialization and cannot be modified afterward. Use the readonly modifier:

Readonly Properties:

// Type with readonly properties
type Config = {
  readonly apiKey: string;
  readonly baseUrl: string;
  readonly timeout: number;
  retryAttempts: number;  // Not readonly, can be modified
};

const config: Config = {
  apiKey: 'secret-key-123',
  baseUrl: 'https://api.example.com',
  timeout: 5000,
  retryAttempts: 3
};

// Valid: modifying non-readonly property
config.retryAttempts = 5;

// Error: Cannot assign to readonly property
// config.apiKey = 'new-key';
// config.baseUrl = 'https://new-api.com';

// Readonly with nested objects
type Article = {
  readonly id: string;
  title: string;
  content: string;
  readonly metadata: {
    readonly createdAt: Date;
    readonly author: string;
    views: number;  // Not readonly
  };
};

const article: Article = {
  id: 'article-001',
  title: 'TypeScript Guide',
  content: 'Learn TypeScript...',
  metadata: {
    createdAt: new Date(),
    author: 'John Doe',
    views: 0
  }
};

// Valid: modifying non-readonly nested property
article.metadata.views = 100;

// Valid: modifying non-readonly top-level property
article.title = 'Complete TypeScript Guide';

// Error: Cannot modify readonly properties
// article.id = 'new-id';
// article.metadata.createdAt = new Date();
// article.metadata.author = 'Jane Doe';

// Readonly arrays
type TodoList = {
  readonly items: readonly string[];
};

const todos: TodoList = {
  items: ['Task 1', 'Task 2', 'Task 3']
};

// Error: Cannot modify readonly array
// todos.items.push('Task 4');
// todos.items[0] = 'Updated Task';
Warning: readonly is a compile-time check only. It doesn't make objects immutable at runtime. If you need true immutability, consider using Object.freeze() or immutable data structures.

Index Signatures

Index signatures allow you to describe objects that can have properties with dynamic keys. This is useful when you don't know all property names in advance:

Index Signatures:

// String index signature
type StringDictionary = {
  [key: string]: string;
};

const translations: StringDictionary = {
  hello: 'Hola',
  goodbye: 'Adiós',
  thanks: 'Gracias'
};

// Add new properties dynamically
translations.welcome = 'Bienvenido';
console.log(translations.hello); // "Hola"

// Number index signature
type NumberArray = {
  [index: number]: string;
};

const fruits: NumberArray = {
  0: 'Apple',
  1: 'Banana',
  2: 'Orange'
};

console.log(fruits[1]); // "Banana"

// Index signature with known properties
type UserData = {
  name: string;           // Known property
  email: string;          // Known property
  [key: string]: string;  // Additional dynamic properties
};

const userData: UserData = {
  name: 'Alice',
  email: 'alice@example.com',
  phone: '+1234567890',      // Dynamic property
  address: '123 Main St'     // Dynamic property
};

// Mixed value types with index signature
type MixedData = {
  [key: string]: string | number | boolean;
};

const settings: MixedData = {
  theme: 'dark',
  fontSize: 16,
  autoSave: true,
  language: 'en'
};

// Readonly index signature
type ReadonlyDictionary = {
  readonly [key: string]: number;
};

const scores: ReadonlyDictionary = {
  math: 95,
  science: 88,
  history: 92
};

// Error: Cannot modify properties in readonly index signature
// scores.math = 100;
Note: When using index signatures, all explicitly declared properties must match the index signature type. For example, if you have [key: string]: number, you cannot add a property with a string value.

Excess Property Checks

TypeScript performs excess property checks when assigning object literals to typed variables. This prevents typos and ensures objects have only the expected properties:

Excess Property Checks:

type User = {
  name: string;
  age: number;
};

// Valid: object has exactly the expected properties
const user1: User = {
  name: 'Alice',
  age: 30
};

// Error: Excess property 'email' not in type 'User'
// const user2: User = {
//   name: 'Bob',
//   age: 25,
//   email: 'bob@example.com'  // Excess property
// };

// Workaround 1: Type assertion (use sparingly)
const user3: User = {
  name: 'Charlie',
  age: 35,
  email: 'charlie@example.com'
} as User;

// Workaround 2: Assign to variable first
const tempUser = {
  name: 'David',
  age: 28,
  email: 'david@example.com'
};
const user4: User = tempUser; // No error, excess property check bypassed

// Workaround 3: Use index signature
type FlexibleUser = {
  name: string;
  age: number;
  [key: string]: unknown;  // Allow additional properties
};

const user5: FlexibleUser = {
  name: 'Eve',
  age: 32,
  email: 'eve@example.com',    // Now allowed
  phone: '+1234567890'         // Now allowed
};

// Function parameters also trigger excess property checks
function createUser(user: User): void {
  console.log(`Created user: ${user.name}`);
}

// Error: Excess property 'email'
// createUser({
//   name: 'Frank',
//   age: 40,
//   email: 'frank@example.com'
// });

// Valid: exact properties
createUser({
  name: 'Grace',
  age: 27
});
Tip: Excess property checks only apply to object literals. If you assign an object to a variable first, TypeScript uses structural typing and allows extra properties.

Nested Object Types

Objects can contain other objects, creating nested structures:

Nested Object Types:

// Deeply nested object type
type Company = {
  name: string;
  address: {
    street: string;
    city: string;
    country: string;
    coordinates: {
      latitude: number;
      longitude: number;
    };
  };
  employees: {
    count: number;
    departments: {
      name: string;
      headCount: number;
    }[];
  };
  contact: {
    email: string;
    phone: string;
    social: {
      twitter?: string;
      linkedin?: string;
      github?: string;
    };
  };
};

const company: Company = {
  name: 'Tech Corp',
  address: {
    street: '100 Tech Avenue',
    city: 'San Francisco',
    country: 'USA',
    coordinates: {
      latitude: 37.7749,
      longitude: -122.4194
    }
  },
  employees: {
    count: 500,
    departments: [
      { name: 'Engineering', headCount: 200 },
      { name: 'Sales', headCount: 150 },
      { name: 'Marketing', headCount: 100 }
    ]
  },
  contact: {
    email: 'info@techcorp.com',
    phone: '+1-555-0123',
    social: {
      twitter: '@techcorp',
      linkedin: 'techcorp'
    }
  }
};

// Accessing nested properties
console.log(company.address.city);                    // "San Francisco"
console.log(company.address.coordinates.latitude);    // 37.7749
console.log(company.employees.departments[0].name);   // "Engineering"
console.log(company.contact.social.twitter);          // "@techcorp"

// Optional chaining with nested objects
console.log(company.contact.social.github ?? 'Not provided');

Object Type Utilities

TypeScript provides built-in utility types for working with object types:

Built-in Object Utilities:

// Original type
type User = {
  id: string;
  name: string;
  email: string;
  age: number;
  isActive: boolean;
};

// Partial<T> - Makes all properties optional
type PartialUser = Partial<User>;

const updateUser: PartialUser = {
  name: 'Alice',
  age: 31
  // Other properties are optional
};

// Required<T> - Makes all properties required
type OptionalUser = {
  name?: string;
  email?: string;
  age?: number;
};

type RequiredUser = Required<OptionalUser>;

const fullUser: RequiredUser = {
  name: 'Bob',
  email: 'bob@example.com',
  age: 25  // All properties now required
};

// Readonly<T> - Makes all properties readonly
type ReadonlyUser = Readonly<User>;

const user: ReadonlyUser = {
  id: '123',
  name: 'Charlie',
  email: 'charlie@example.com',
  age: 30,
  isActive: true
};

// Error: Cannot modify readonly properties
// user.name = 'David';

// Pick<T, K> - Creates type with subset of properties
type UserPreview = Pick<User, 'id' | 'name' | 'email'>;

const preview: UserPreview = {
  id: '456',
  name: 'Eve',
  email: 'eve@example.com'
  // age and isActive not needed
};

// Omit<T, K> - Creates type excluding specified properties
type UserWithoutId = Omit<User, 'id' | 'isActive'>;

const newUser: UserWithoutId = {
  name: 'Frank',
  email: 'frank@example.com',
  age: 35
  // id and isActive excluded
};

// Record<K, T> - Creates object type with specific keys and value type
type UserRoles = Record<string, boolean>;

const roles: UserRoles = {
  admin: true,
  editor: false,
  viewer: true
};

// Combining utilities
type PartialReadonlyUser = Partial<Readonly<User>>;
type RequiredUserPreview = Required<Pick<User, 'name' | 'email'>>;

Object Destructuring with Types

TypeScript works seamlessly with object destructuring:

Destructuring with Types:

type Product = {
  id: string;
  name: string;
  price: number;
  category: string;
  inStock: boolean;
};

// Destructuring in function parameters
function displayProduct({ name, price, inStock }: Product): void {
  console.log(`Product: ${name}`);
  console.log(`Price: $${price}`);
  console.log(`In Stock: ${inStock ? 'Yes' : 'No'}`);
}

const laptop: Product = {
  id: 'prod-001',
  name: 'MacBook Pro',
  price: 2499,
  category: 'Electronics',
  inStock: true
};

displayProduct(laptop);

// Destructuring with renaming
function processUser({ name: userName, email: userEmail }: {
  name: string;
  email: string;
}): void {
  console.log(`User: ${userName}`);
  console.log(`Email: ${userEmail}`);
}

// Nested destructuring
type Order = {
  id: string;
  customer: {
    name: string;
    email: string;
  };
  items: {
    product: string;
    quantity: number;
  }[];
};

function processOrder({
  id,
  customer: { name, email },
  items
}: Order): void {
  console.log(`Order ID: ${id}`);
  console.log(`Customer: ${name} (${email})`);
  console.log(`Items: ${items.length}`);
}
Exercise:
  1. Create a type Book with properties: readonly id: string, title: string, author: string, publishedYear: number, optional isbn?: string, and optional tags?: string[]
  2. Create a type Library with a string index signature that maps book IDs to Book objects, plus a property name: string
  3. Create a function addBook that takes a Library and a Book, adds the book to the library, and returns the updated library
  4. Create a type BookSummary using Pick to extract only title, author, and publishedYear from Book
  5. Create instances of Book, Library, and test your addBook function

Summary

  • Object type annotation describes the shape and structure of objects
  • Optional properties use ? to make properties optional
  • Readonly properties use readonly to prevent modification after initialization
  • Index signatures allow objects with dynamic property names
  • Excess property checks prevent typos by rejecting unexpected properties in object literals
  • Nested objects create complex hierarchical data structures
  • Utility types like Partial, Required, Readonly, Pick, and Omit transform object types
  • Destructuring works seamlessly with TypeScript types