1. Interfaces
An interface describes the shape an object must have — which properties
exist, and what type each one is:
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:
type User = {
id: number;
name: string;
isActive: boolean;
};
const user: User = { id: 1, name: "Priya", isActive: true }; // works identically to the interface
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.
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:
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":
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'
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
Model a small blog post system
Practice interfaces, optional/readonly properties, and union types together.
Requirements:
- Define an
interface Authorwithreadonly id: numberandname: string. - Define a
type PostStatus = "draft" | "published" | "archived". - Define an
interface Postwithreadonly id: number,title: string,status: PostStatus, an optionalsummary?: string, and anauthor: Author(a nested interface). - Write a function
publishPost(post: Post): Postthat returns a new object withstatusset to"published", without mutating the original (reuse Week 6's spread pattern). - Create at least two
Postobjects (one with asummary, one without), pass both throughpublishPost, and confirm the compiler rejects an attempt to reassign a post'siddirectly.
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?
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?
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?
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?
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.