On this page
Interfaces and type aliases
Describing object shapes
When working with objects in TypeScript, you need a way to describe their structure: which properties exist, what types they have, which are optional, and which should never be modified after initialization. TypeScript provides two mechanisms: interfaces and type aliases.
Interfaces
An interface declares the shape of an object. It specifies property names and their types, whether properties are required or optional, and whether they are readonly.
interface BlogPost {
id: number;
title: string;
body: string;
publishedAt: Date;
tags: string[];
}Any object that has at least these properties with the correct types is a valid BlogPost. TypeScript uses structural typing — the concrete type of the object does not matter, only its shape.
const post: BlogPost = {
id: 1,
title: "Getting started with TypeScript",
body: "...",
publishedAt: new Date(),
tags: ["typescript", "tutorial"],
};Optional properties
Add ? after the property name to make it optional. An optional property can be undefined or absent entirely.
interface UserProfile {
id: number;
name: string;
bio?: string; // may be undefined
avatarUrl?: string; // may be undefined
}
const user: UserProfile = { id: 1, name: "Alice" }; // bio and avatarUrl omitted — OKWhen accessing an optional property, TypeScript narrows the type to T | undefined and forces you to handle the missing case:
function displayBio(profile: UserProfile): string {
// profile.bio is string | undefined here
return profile.bio ?? "No bio provided";
}Readonly properties
The readonly modifier prevents a property from being reassigned after the object is created.
interface Config {
readonly apiBaseUrl: string;
readonly maxRetries: number;
timeout: number; // mutable
}
const config: Config = {
apiBaseUrl: "https://api.example.com",
maxRetries: 3,
timeout: 5000,
};
config.timeout = 10000; // OK — timeout is mutable
config.maxRetries = 5; // Error: Cannot assign to 'maxRetries' because it is a read-only propertyExtending interfaces
Interfaces can inherit from one or more other interfaces using extends:
interface Animal {
name: string;
sound(): string;
}
interface Pet extends Animal {
owner: string;
}
interface ServiceAnimal extends Pet {
certificationNumber: string;
tasks: string[];
}
const guide: ServiceAnimal = {
name: "Rex",
owner: "Alice",
certificationNumber: "SA-2024-001",
tasks: ["navigate", "alert"],
sound() { return "Woof"; },
};Multiple inheritance is allowed with interfaces: interface C extends A, B { ... }.
Declaration merging
One unique feature of interfaces is declaration merging: if you declare an interface with the same name twice, TypeScript merges the two declarations into a single interface:
interface Window {
title: string;
}
interface Window {
statusBar: string; // merged with the first declaration
}
// Now Window has both title and statusBar
const win: Window = { title: "My App", statusBar: "Ready" };This is heavily used to extend third-party type definitions. For example, to add a custom property to Express's Request object you merge into its interface. Inside your own application code, prefer extends over merging for clarity.
Type aliases
A type alias assigns a name to any TypeScript type — not just object shapes. This is the key distinction from interfaces.
// Alias for a primitive
type UserId = number;
type Email = string;
// Alias for a union
type Theme = "light" | "dark" | "system";
// Alias for a tuple
type Point = [x: number, y: number];
// Alias for a function type
type Comparator<T> = (a: T, b: T) => number;
// Alias for an object shape
type Address = {
street: string;
city: string;
country: string;
postalCode?: string;
};Intersection types with &
While interfaces use extends for inheritance, type aliases combine types using the & operator, called an intersection:
type Timestamped = {
createdAt: Date;
updatedAt: Date;
};
type WithAuthor = {
authorId: number;
authorName: string;
};
type Article = {
title: string;
body: string;
} & Timestamped & WithAuthor;
const article: Article = {
title: "TypeScript in 2026",
body: "...",
createdAt: new Date(),
updatedAt: new Date(),
authorId: 42,
authorName: "Alice",
};Intersections are additive — the resulting type has all properties from all intersected types. If two intersected types declare the same property name with incompatible types, the resulting property type becomes never.
Index signatures
When an object can have an arbitrary number of properties where all values have the same type, use an index signature:
interface TranslationMap {
[key: string]: string;
}
const translations: TranslationMap = {
"greeting": "Hello",
"farewell": "Goodbye",
};
// Dynamically access properties
function translate(key: string, map: TranslationMap): string {
return map[key] ?? `[${key}]`;
}The built-in Record<K, V> utility type is a cleaner alternative when you know the set of allowed keys:
// Only "en" | "es" | "fr" are valid keys
type SupportedLocale = "en" | "es" | "fr";
type LocalizedStrings = Record<SupportedLocale, string>;
const greetings: LocalizedStrings = {
en: "Hello",
es: "Hola",
fr: "Bonjour",
};interface vs type: when to use each
Both work for describing object shapes. The practical differences are small but important:
| Feature | interface | type |
|---|---|---|
| Object shapes | Yes | Yes |
| Unions and intersections | No | Yes |
| Primitive aliases | No | Yes |
| Tuple types | No | Yes |
| Declaration merging | Yes | No |
extends keyword |
Yes | No (use &) |
| Error messages | Often clearer | Sometimes verbose |
Rule of thumb: Use interface for the public API of a class or module — anything you expect consumers to extend or implement. Use type for unions, utility types, computed types, and internal shapes that do not need extension.
Practice
- Define an
interface Productwith required and optional properties. Extend it to create aninterface DigitalProductthat adds adownloadUrlfield. Create a value of each type and verify TypeScript enforces the shapes. - Use intersection types to compose a
type AuditedRecordby combining a base type with aTimestampedtype. Write a factory function that creates anAuditedRecordfrom a plain object. - Implement an index signature for a simple in-memory cache:
interface Cache { [key: string]: unknown }. Write a typed get function that accepts a generic type parameter and casts the retrieved value.
// Interface: describes the shape of an object
interface User {
readonly id: number;
name: string;
email: string;
age?: number; // optional property
}
// Extending an interface
interface AdminUser extends User {
role: "admin";
permissions: string[];
}
// Declaration merging — interfaces can be reopened
interface User {
createdAt: Date; // merged into the original User interface
}
const admin: AdminUser = {
id: 1,
name: "Alice",
email: "[email protected]",
role: "admin",
permissions: ["read", "write", "delete"],
createdAt: new Date(),
};
Sign in to track your progress