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:
// 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 }'
};interface
An interface names and reuses an object type. It is the idiomatic TypeScript way to describe the shape of an object:
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:
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:
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: readonlyreadonly 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:
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:
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 DogIntersection Types
The type alias equivalent of extends is the intersection operator &. It combines multiple types into one that has all properties of every constituent:
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:
// 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.
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 worksExcess 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:
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 propertiesNested Object Types
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:
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
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
// 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
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
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 falseSummary
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