TypeScriptObject Types

Object Types

Objects are the backbone of JavaScript programs. TypeScript adds precise type information to objects — what properties they have, what types those properties hold, which are optional, and which cannot be changed. This page covers the full range of object typing in TypeScript, from inline shapes to reusable interfaces and type aliases.

Inline Object Type Annotation

The simplest way to type an object is to describe its shape inline, directly in the annotation:

TS
// Inline object type
let user: { name: string; age: number; active: boolean } = {
  name: 'Alice',
  age: 30,
  active: true,
};

// Accessing properties — fully typed
const greeting = `Hello, ${user.name}`;  // string
user.age += 1;                              // number

// TypeScript catches extra or missing properties
let invalid: { name: string; age: number } = {
  name: 'Bob',
  age: 25,
  role: 'admin',  // Error: 'role' does not exist in type '{ name: string; age: number }'
};
Note
Inline object types work for simple one-off cases, but quickly become verbose and hard to reuse. For anything you reference more than once, define an interface or type alias.
interface

An interface names and reuses an object type. It is the idiomatic TypeScript way to describe the shape of an object:

TS
interface User {
  id: number;
  name: string;
  email: string;
  active: boolean;
}

// Use the interface as a type
const alice: User = {
  id: 1,
  name: 'Alice',
  email: 'alice@example.com',
  active: true,
};

// As a function parameter
function greetUser(user: User): string {
  return `Hello, ${user.name}!`;
}

// As a function return type
function createUser(name: string, email: string): User {
  return { id: Math.random(), name, email, active: true };
}
Optional Properties

Add a ? to mark a property as optional. Optional properties have type T | undefined and may be omitted entirely from an object literal:

TS
interface BlogPost {
  id: number;
  title: string;
  content: string;
  publishedAt?: Date;     // optional — may not exist yet
  tags?: string[];        // optional — might have no tags
  featuredImage?: string; // optional — some posts have no image
}

// All of these are valid BlogPost values
const draft: BlogPost = { id: 1, title: 'Draft', content: '...' };
const published: BlogPost = {
  id: 2,
  title: 'Published',
  content: '...',
  publishedAt: new Date(),
  tags: ['typescript', 'tutorial'],
};

// Accessing optional properties safely
function getPublishDate(post: BlogPost): string {
  // post.publishedAt is Date | undefined — must handle both cases
  return post.publishedAt?.toISOString() ?? 'Not published yet';
}
readonly Properties

Mark a property readonly to prevent reassignment after the object is created. This is the object equivalent of const for a variable:

TS
interface Config {
  readonly apiUrl: string;
  readonly version: number;
  timeout: number;  // mutable — can be changed at runtime
}

const config: Config = {
  apiUrl: 'https://api.example.com',
  version: 2,
  timeout: 5000,
};

config.timeout = 10000;  // OK — not readonly
config.apiUrl = 'https://other.com';  // Error: Cannot assign to 'apiUrl' because it is a read-only property
config.version = 3;                   // Error: readonly
Tip
readonly prevents reassignment but does not deep-freeze objects. A readonly array property can still have elements pushed into it unless the array itself is also declared readonly.
type Alias for Objects

A type alias can also describe object shapes. The syntax is nearly identical to interface:

TS
type Point = {
  x: number;
  y: number;
};

type Rectangle = {
  topLeft: Point;
  width: number;
  height: number;
};

const rect: Rectangle = {
  topLeft: { x: 0, y: 0 },
  width: 100,
  height: 50,
};
interface vs type — Practical Differences

Feature

interface

type

Object shape

Yes

Yes

Extend / inherit

extends keyword

Intersection & (merges shapes)

Declaration merging

Yes — can be declared twice to extend

No — second declaration is an error

Union types

No

Yes — type X = A | B

Primitive aliases

No

Yes — type ID = string

Computed property names

Limited

Full support

Error messages

Shows interface name

Shows expanded type

As a rule of thumb: use interface for object shapes in public APIs and class definitions, and use type when you need unions, intersections, primitives, or utility types. Many codebases use type everywhere — both choices are defensible.

Extending Interfaces

Interfaces can extend one or more other interfaces, inheriting all their properties:

TS
interface Animal {
  name: string;
  age: number;
}

interface Dog extends Animal {
  breed: string;
  trained: boolean;
}

interface ServiceDog extends Dog {
  certificationNumber: string;
  trainedFor: 'guide' | 'hearing' | 'mobility';
}

const scout: ServiceDog = {
  name: 'Scout',
  age: 3,
  breed: 'Labrador',
  trained: true,
  certificationNumber: 'SD-2024-001',
  trainedFor: 'guide',
};

// A ServiceDog is also a Dog and an Animal — structural compatibility
function greetDog(dog: Dog) {
  console.log(`Good dog, ${dog.name}!`);
}
greetDog(scout);  // OK — ServiceDog satisfies Dog
Intersection Types

The type alias equivalent of extends is the intersection operator &. It combines multiple types into one that has all properties of every constituent:

TS
type Timestamped = {
  createdAt: Date;
  updatedAt: Date;
};

type SoftDeletable = {
  deletedAt: Date | null;
};

type BaseModel = Timestamped & SoftDeletable;

type Product = BaseModel & {
  id: number;
  name: string;
  price: number;
};

const product: Product = {
  id: 1,
  name: 'Widget',
  price: 9.99,
  createdAt: new Date(),
  updatedAt: new Date(),
  deletedAt: null,
};
Index Signatures

When you don't know the property names in advance but you know their types, use an index signature:

TS
// Object with any string keys, all values are numbers
interface ScoreMap {
  [player: string]: number;
}

const scores: ScoreMap = {};
scores['Alice'] = 95;
scores['Bob'] = 87;

// The index signature applies to ALL properties
const aliceScore: number = scores['Alice'];  // OK

// Can combine with known properties — known properties must match the index type
interface Config {
  [key: string]: string | number;  // index signature
  host: string;    // OK — string matches string | number
  port: number;    // OK — number matches string | number
  // active: boolean; // Error — boolean does not match string | number
}
Structural Typing

TypeScript uses structural typing (also called "duck typing"). A value satisfies a type if it has at least the required properties — it does not matter what the value was declared as or what class it was constructed from.

TS
interface Printable {
  toString(): string;
}

// These are completely different types, but both satisfy Printable
class Invoice {
  constructor(public id: number, public total: number) {}
  toString() { return `Invoice #${this.id}: $${this.total}`; }
}

class User {
  constructor(public name: string) {}
  toString() { return `User: ${this.name}`; }
}

function print(item: Printable) {
  console.log(item.toString());
}

print(new Invoice(1, 99.99));  // OK
print(new User('Alice'));      // OK
print({ toString: () => 'custom' });  // OK — any object with toString works
Excess Property Checking

TypeScript performs stricter checks when you pass an object literal directly. Extra properties in a literal cause an error — but the same object assigned to a variable first does not:

TS
interface Point {
  x: number;
  y: number;
}

function plotPoint(p: Point) {
  console.log(p.x, p.y);
}

// Direct literal — excess property check triggers
plotPoint({ x: 1, y: 2, z: 3 });
// Error: Argument of type '{ x: number; y: number; z: number; }'
// is not assignable to parameter of type 'Point'.
// Object literal may only specify known properties, and 'z' does not exist in type 'Point'.

// Assign first — structural check only (no excess property check)
const p = { x: 1, y: 2, z: 3 };
plotPoint(p);  // OK — p has at least the required properties
Note
Excess property checking is a deliberate design choice. It catches typos in object literals (a very common class of bug) while still allowing structural compatibility for variables passed indirectly.
Nested Object Types

TS
interface Address {
  street: string;
  city: string;
  country: string;
  postalCode: string;
}

interface Company {
  name: string;
  industry: string;
  headquarters: Address;  // nested object
  offices: Address[];     // array of nested objects
}

const acme: Company = {
  name: 'Acme Corp',
  industry: 'Technology',
  headquarters: {
    street: '123 Main St',
    city: 'Springfield',
    country: 'US',
    postalCode: '12345',
  },
  offices: [
    { street: '1 Tech Blvd', city: 'San Francisco', country: 'US', postalCode: '94107' },
  ],
};

// Deep access is fully typed
const hqCity: string = acme.headquarters.city;
const firstOffice: Address = acme.offices[0];
Partial, Required, and Readonly Utility Types

TypeScript's built-in utility types transform object types without rewriting them:

TS
interface User {
  id: number;
  name: string;
  email: string;
  age: number;
}

// Partial<T> — makes all properties optional (useful for update payloads)
type UserUpdate = Partial<User>;
// { id?: number; name?: string; email?: string; age?: number }

// Required<T> — makes all properties required (removes optionality)
interface DraftPost {
  title?: string;
  content?: string;
}
type PublishedPost = Required<DraftPost>;
// { title: string; content: string }

// Readonly<T> — makes all properties readonly
type FrozenUser = Readonly<User>;
// { readonly id: number; readonly name: string; ... }

// Usage
function updateUser(id: number, changes: Partial<User>): void {
  // changes can have any subset of User properties
}
Pick and Omit Utility Types

TS
interface User {
  id: number;
  name: string;
  email: string;
  passwordHash: string;
  createdAt: Date;
}

// Pick<T, K> — keep only specified keys
type PublicUser = Pick<User, 'id' | 'name' | 'email'>;
// { id: number; name: string; email: string }

// Omit<T, K> — remove specified keys
type UserWithoutPassword = Omit<User, 'passwordHash'>;
// { id: number; name: string; email: string; createdAt: Date }

// Common pattern: omit auto-generated fields for creation payloads
type CreateUserPayload = Omit<User, 'id' | 'createdAt'>;
// { name: string; email: string; passwordHash: string }

function createUser(payload: CreateUserPayload): User {
  return {
    ...payload,
    id: generateId(),
    createdAt: new Date(),
  };
}
Record Utility Type

TS
// Record<Keys, Values> — object type with specific key and value types
type Role = 'admin' | 'editor' | 'viewer';

type Permissions = Record<Role, boolean>;
// { admin: boolean; editor: boolean; viewer: boolean }

const permissions: Permissions = {
  admin: true,
  editor: true,
  viewer: false,
};

// Dynamic key type from a union
type CountryCode = 'US' | 'GB' | 'DE' | 'FR';
type CountryName = Record<CountryCode, string>;

const countries: CountryName = {
  US: 'United States',
  GB: 'United Kingdom',
  DE: 'Germany',
  FR: 'France',
};
Object Destructuring with Types

TS
interface Config {
  host: string;
  port: number;
  ssl: boolean;
  timeout: number;
}

// Destructure — types are inferred from the interface
const { host, port, ssl, timeout } = config;
// host: string, port: number, ssl: boolean, timeout: number

// Rename on destructure
const { host: serverHost, port: serverPort } = config;
// serverHost: string, serverPort: number

// Default values
function connect({ host, port = 3000, ssl = false }: Partial<Config>) {
  // port and ssl have defaults — they are number and boolean, not undefined
}
Object Spread and Type Merging

TS
interface BaseConfig {
  debug: boolean;
  logLevel: 'error' | 'warn' | 'info';
}

interface DatabaseConfig {
  host: string;
  port: number;
}

// Spread merges both shapes
const config = {
  ...({ debug: true, logLevel: 'info' } as BaseConfig),
  ...({ host: 'localhost', port: 5432 } as DatabaseConfig),
};
// type: BaseConfig & DatabaseConfig

// Immutable update pattern — spread then override
const updated = { ...config, debug: false };
// type still includes all properties, debug is now false
Summary
  • Inline object types describe shapes directly — good for one-off annotations

  • interface names and reuses an object shape — the standard approach for domain models

  • type alias works too — use it when you need unions, intersections, or utility types

  • Optional properties (?) may be omitted — access them with optional chaining

  • readonly properties cannot be reassigned after the object is created

  • TypeScript uses structural typing — a value is compatible if it has the required properties

  • Excess property checking catches typos in object literals but not in variable assignments

  • Use Partial, Required, Readonly, Pick, Omit, and Record to derive new types from existing ones