Foundations Updated 2026-09 View as Markdown

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.

typescript
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:

typescript
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#

typescript
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#

typescript
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:

typescript
interface Lookup {
  [key: string]: number;
}

Composition#

Interfaces extend:

typescript
interface Entity {
  id: string;
  createdAt: Date;
}

interface User extends Entity {
  email: string;
}

Type aliases intersect, which achieves the same thing:

typescript
type Entity = { id: string; createdAt: Date };
type User = Entity & { email: string };

Both compose several sources:

typescript
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:

typescript
type Status = "pending" | "shipped" | "cancelled";
type Id = string | number;
type Handler = (e: Event) => void;      // and function types read better

Only interface supports declaration merging — two declarations with the same name combine:

typescript
interface Window { myApp: AppState }     // adds to the existing DOM Window

That 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:

typescript
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 y

This is usually what you want, and it is occasionally not:

typescript
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#

typescript
// 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 here

Common 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.

Unsubscribe in one click. We never sell the list. Or just take the AGENTS.md now — no email needed.