Week 12: Interfaces & Type Aliases

Week 11 typed individual values. This week types the shape of data — the objects that flow through a real application, like a user record or an API response. Interfaces and type aliases both describe that shape; this week builds both, and is honest about the small handful of situations where they genuinely differ.

Phase 7 of 8 Week 12 of 14 ~3–4 Hours Hands-on Exercise Included

By the end of this week, you'll be able to

  • Define an object's shape with an interface or a type alias
  • Mark properties optional or readonly, and know what each actually enforces
  • Combine types with unions (|) and intersections (&)

1. Interfaces

An interface describes the shape an object must have — which properties exist, and what type each one is:

defining and using an interface
interface User {
  id: number;
  name: string;
  isActive: boolean;
}

function greet(user: User): string {
  return `Hello, ${user.name}!`;
}

const validUser: User = { id: 1, name: "Priya", isActive: true };
greet(validUser); // "Hello, Priya!"

const invalidUser: User = { id: 2, name: "Ben" };
// ERROR: Property 'isActive' is missing in type '{ id: number; name: string; }'

Any object assigned to a User-typed variable, or passed where a User parameter is expected, must have exactly the properties the interface declares (with matching types) — invalidUser above fails to compile because it's missing isActive entirely. This is the core value of typing object shapes: a typo'd or forgotten property is caught immediately, instead of surfacing later as undefined somewhere deep in the code.

2. Type Aliases

A type alias gives a name to any type — including, but not limited to, an object shape:

a type alias for an object shape
type User = {
  id: number;
  name: string;
  isActive: boolean;
};

const user: User = { id: 1, name: "Priya", isActive: true }; // works identically to the interface
type aliases can also name non-object types
type ID = number | string;           // a union (Section 5) -- interfaces can't do this
type Point = [number, number];        // a tuple
type Handler = (event: string) => void; // a function shape

For a plain object shape like User, an interface and a type alias are functionally interchangeable — the choice barely matters. Where they genuinely differ is that a type alias can name any type at all — a union, a tuple, a function signature — while an interface is specifically for describing object (and class) shapes.

3. Interfaces vs. Type Aliases: When to Use Which

Since both work for plain object shapes, the real difference that matters day to day is one specific capability interfaces have that type aliases don't: declaration merging.

interfaces can be reopened and extended
interface User {
  id: number;
  name: string;
}

// Later -- possibly in a different file -- this ADDS to the same User interface
interface User {
  isActive: boolean;
}

const user: User = { id: 1, name: "Priya", isActive: true }; // User now requires all three

// The equivalent with `type` is a compile ERROR: Duplicate identifier 'User'
// type User = { id: number; name: string; };
// type User = { isActive: boolean; };

This matters most when extending a type defined elsewhere — a library's own types, for instance. As a practical default: reach for interface when defining the shape of an object (especially one a class might implement, or a library consumer might need to extend), and reach for type for unions, tuples, function signatures, or anything that isn't a plain object shape.

4. Optional & Readonly Properties

Two modifiers make an object shape more precise about which properties are required and which can change:

optional (?) and readonly properties
interface Product {
  readonly id: number;   // can be read, but never reassigned after creation
  name: string;
  description?: string;   // optional -- may be omitted entirely
}

const product: Product = { id: 1, name: "Notebook" }; // fine -- description is optional

product.name = "Pen";  // fine -- name is not readonly
product.id = 2;         // ERROR: Cannot assign to 'id' because it is a read-only property

? (the same syntax as Week 11's optional function parameters) marks a property as not required — its type inside the object becomes a union with undefined. readonly allows the property to be set once, when the object is created, but blocks any reassignment after that — useful for data like an ID that should genuinely never change once an object exists.

5. Union & Intersection Types

Two operators combine existing types into new ones — | means "either one," and & means "both, combined":

union types: |
type Status = "pending" | "active" | "completed"; // a value must be exactly one of these

function setStatus(status: Status) {
  console.log(`Status set to: ${status}`);
}

setStatus("active");    // fine
setStatus("cancelled"); // ERROR: Argument of type '"cancelled"' is not assignable to type 'Status'
intersection types: &
interface HasId {
  id: number;
}

interface HasTimestamps {
  createdAt: Date;
  updatedAt: Date;
}

type Record = HasId & HasTimestamps; // must satisfy BOTH interfaces at once

const record: Record = {
  id: 1,
  createdAt: new Date(),
  updatedAt: new Date(),
}; // needs every property from both HasId AND HasTimestamps

A union (Status above) restricts a value to one of a fixed set of options — this specific pattern, a union of exact string literals, is how TypeScript expresses an enum-like "must be one of these exact values" constraint without a separate enum keyword (Week 13 covers real enums too). An intersection (Record above) merges multiple shapes together — the resulting type requires every property from all of them combined, not a choice between them.

6. Hands-on Exercise

Hands-on

Model a small blog post system

Practice interfaces, optional/readonly properties, and union types together.

Requirements:

  1. Define an interface Author with readonly id: number and name: string.
  2. Define a type PostStatus = "draft" | "published" | "archived".
  3. Define an interface Post with readonly id: number, title: string, status: PostStatus, an optional summary?: string, and an author: Author (a nested interface).
  4. Write a function publishPost(post: Post): Post that returns a new object with status set to "published", without mutating the original (reuse Week 6's spread pattern).
  5. Create at least two Post objects (one with a summary, one without), pass both through publishPost, and confirm the compiler rejects an attempt to reassign a post's id directly.
Hint

Step 4's spread pattern: return {'{'} ...post, status: "published" {'}'} — this is the exact same immutable-update shape from Week 6, TypeScript just verifies the result still matches the Post interface.

7. Knowledge Check

Four quick questions. Expand each to check your answer.

Q1

For a plain object shape, is there a meaningful runtime difference between using interface and type?

No — both are erased entirely at compile time and produce identical JavaScript output either way. The practical difference is at the type-checking level: interfaces support declaration merging (reopening the same name to add more properties), which type aliases explicitly disallow.

Q2

What's the difference between a property marked ? (optional) and one marked readonly?

? controls whether the property has to be present at all — an optional property may be entirely omitted when the object is created. readonly controls whether it can be reassigned after the object is created — a readonly property must still be set once initially, but can never be changed afterward. They address different questions and are often used independently of each other.

Q3

Given type Status = "pending" | "active" | "completed", what happens if you try to assign "cancelled" to a Status-typed variable?

A compile error. A union of string literals restricts a value to exactly one of the listed options — "cancelled" isn't one of "pending", "active", or "completed", so TypeScript rejects it, the same way it would reject any other type mismatch.

Q4

Given type Combined = HasId & HasName, does an object need to satisfy HasId, HasName, or both to be assignable to Combined?

Both. An intersection type (&) merges the two shapes together — the resulting type requires every property from both HasId and HasName at once. This is the opposite of a union (|), which requires satisfying only one of the combined options.