Interfaces and Type Aliases
Naming the shape of your data is the point at which TypeScript starts paying for itself.
Once you are past primitives, most typing is describing the shape of objects. There are two ways to name a shape, and they overlap almost entirely.
interface User {
id: string;
email: string;
age: number;
}
type Product = {
id: string;
name: string;
priceCents: number;
};Both work the same way at the point of use:
const ada: User = { id: "1", email: "ada@example.com", age: 36 };
const wrong: User = { id: "1", email: "ada@example.com" };
// Property 'age' is missing in type '{ id: string; email: string; }'Optional and readonly#
interface User {
id: string;
email: string;
age?: number; // may be absent — type is number | undefined
readonly createdAt: Date; // cannot be reassigned after construction
}
const u: User = { id: "1", email: "a@b.com", createdAt: new Date() };
u.createdAt = new Date(); // error: Cannot assign to 'createdAt'readonly is shallow — it stops reassignment of the property, not mutation of the object it points at.
Nested and array properties#
interface Order {
id: string;
customer: User; // another named shape
lines: OrderLine[]; // an array of them
metadata: Record<string, string>; // arbitrary string keys
status: "pending" | "shipped"; // a union of literals
}Record<K, V> is the idiomatic way to say "an object used as a lookup". The longhand is an index signature:
interface Lookup {
[key: string]: number;
}Composition#
Interfaces extend:
interface Entity {
id: string;
createdAt: Date;
}
interface User extends Entity {
email: string;
}Type aliases intersect, which achieves the same thing:
type Entity = { id: string; createdAt: Date };
type User = Entity & { email: string };Both compose several sources:
interface Admin extends Entity, Auditable { role: "admin" }
type Admin = Entity & Auditable & { role: "admin" };Where they genuinely differ#
Only type can express a union, which is why it is the more common default:
type Status = "pending" | "shipped" | "cancelled";
type Id = string | number;
type Handler = (e: Event) => void; // and function types read betterOnly interface supports declaration merging — two declarations with the same name combine:
interface Window { myApp: AppState } // adds to the existing DOM WindowThat is essential for augmenting types from a library you do not control, and a footgun everywhere else, because a name can be extended from anywhere.
Which to use
Use type by default — it covers unions, functions and object shapes uniformly. Use interface when you need declaration merging (augmenting a third-party type) or when you are publishing a type others will extend. Pick one convention per codebase and write it in your instructions file, otherwise generated code will use both interchangeably.
Structural typing#
TypeScript checks shapes, not names. Anything with the right properties satisfies the type:
interface Point { x: number; y: number }
function distance(p: Point): number {
return Math.hypot(p.x, p.y);
}
const anything = { x: 3, y: 4, label: "extra" };
distance(anything); // fine — it has x and yThis is usually what you want, and it is occasionally not:
type UserId = string;
type OrgId = string;
function loadUser(id: UserId) {}
loadUser(someOrgId); // compiles. wrong. both are just strings.Two types with the same underlying shape are interchangeable — which is exactly the bug branded types solve, covered later in the track.
Exercise#
// Model a blog post:
// - id (string, readonly), title (string), body (string)
// - tags: an array of strings
// - publishedAt: a Date that may be absent (drafts)
// - author: a nested shape with name and email
// Then write a `summarize(post)` function returning `"title — N tags"`.
// write your code hereCommon questions#
interface or type — really, which?#
type unless you need declaration merging. It handles unions, function types and object shapes with one keyword, which means fewer decisions. The important part is consistency: a codebase using both at random is harder to read than either choice.
Why was my object with extra properties rejected?#
Excess property checking, which only applies to object literals passed directly. TypeScript assumes an unknown property written inline is a typo. Assigning to a variable first bypasses it, because at that point the object has a known type and the extra property is deliberate.
How do I make a type where all properties are optional?#
Partial<T>. There is a set of built-in utility types — Partial, Required, Readonly, Pick, Omit — covered in their own lesson later in this track.
Get the TypeScript agent pack
A battle-tested AGENTS.md, the review checklist, and the failure-mode cheat sheet for TypeScript. One email, then occasional updates when the tooling shifts. No course pitch.
AGENTS.md now — no email needed.