Lesson 4 • TypeScript
Interfaces and Type Aliases 📐
By the end of this lesson you'll be able to describe the exact shape of any object with an interface — required, optional, and read-only properties, methods, and inheritance — and know exactly when to reach for a type alias instead.
What You'll Learn
- Describe an object's shape with an interface
- Mark properties optional with ? and lock them with readonly
- Add method signatures so objects must provide functions
- Build bigger interfaces by extending smaller ones
- Use index signatures for flexible string-keyed objects
- Choose interface vs type alias for the job
string, number, and boolean annotations. Note: TypeScript compiles to JavaScript, so the runnable demos here are JavaScript that shows each shape at runtime; the read-only blocks show the real TypeScript syntax you'd type in a .ts file. Install TypeScript for full type checking.💡 Real-World Analogy
An interface is a contract — or a blueprint. A house blueprint says "there must be a kitchen, three bedrooms, and a 2 metre door" without laying a single brick. Any builder who follows it produces a house that fits. In the same way, an interface says "any object claiming to be a User must have an id (number), a name (string), and so on." TypeScript then checks every object against that contract before your program runs — so a typo or a missing field is caught at your desk, not by a user.
1. Describing an Object's Shape
An interface is a named description of what an object must contain. You list each property and its type. Once an object is annotated with that interface, TypeScript guarantees every listed property exists and has the right type. Here's the real TypeScript syntax — read every comment:
// An interface describes the SHAPE an object must have.
interface User {
id: number; // required: every User must have a number id
name: string; // required: must be text
email: string; // required: must be text
isActive: boolean; // required: true or false
}
// This object matches the shape, so TypeScript is happy.
const alice: User = {
id: 1,
name: "Alice",
email: "alice@example.com",
isActive: true,
};
// Missing "email" -> error TS2741 (see Common Errors below):
// const bob: User = { id: 2, name: "Bob", isActive: true };At runtime the interface vanishes and you're left with a plain JavaScript object. Run this worked example to see the shape come to life — the comments state exactly what each line prints:
Worked example: a User object
The interface is compile-time only; at runtime it's a normal JS object.
// The interface lives only in TypeScript. At runtime it disappears,
// leaving a plain JS object that MATCHES the shape we promised.
//
// interface User {
// id: number;
// name: string;
// email: string;
// isActive: boolean;
// bio?: string; // optional
// readonly joinedAt: string;
// }
const user = {
id: 1,
name: "Alice",
email: "alice@example.com",
isActive: true,
joinedAt: "2024-03-01", // readonly: we set it once, here
// bio is omitted — that's allowed
...2. Optional ? and readonly Properties
Not every property is required. Add a ? after the name to make a property optional — objects may include it or leave it out. Add readonly before a property to lock it after creation: you can set it once, but any later assignment is a compile error. This is how you model "an id is fixed forever, but a bio can be added later."
interface UserProfile {
name: string; // required
email: string; // required
readonly id: number; // set once, then locked
bio?: string; // optional — the ? means "may be missing"
avatar?: string; // optional
}
const p: UserProfile = { id: 7, name: "Alice", email: "a@ex.com" };
// bio and avatar were omitted — that's fine, they're optional.
p.bio = "Developer"; // ✅ ok to add later
p.id = 99; // ❌ error TS2540: 'id' is read-only and cannot changeYour turn. The program below builds an object that must match a Book shape (with one readonly and one optional field). Fill in the two blanks marked ___, then run it.
🎯 Your turn: build a Book
Fill in the ___ blanks, then check your output against the expected lines.
// 🎯 YOUR TURN — replace each ___ then press "Try it Yourself".
//
// You're modelling a Book for a library app. In TypeScript the shape is:
//
// interface Book {
// readonly isbn: string; // set once, never changes
// title: string; // required
// pages: number; // required
// subtitle?: string; // optional
// }
//
// Build a matching object below.
const book = {
isbn: "978-0131103627",
title: ___, // 👉 a book title in "double quotes"
...3. Method Signatures & Extending Interfaces
Interfaces aren't just for data — they can require methods too. A method signature like speak(): string says "this object must have a function called speak that returns a string." And extends lets you build a bigger interface on top of a smaller one: a Dog gets everything an Animal has, plus its own extras. You can even extend several interfaces at once to compose shapes together.
// Method signatures: describe a function a property must provide.
interface Animal {
name: string;
speak(): string; // a method that takes nothing, returns a string
feed(amount: number): void; // takes a number, returns nothing
}
// "extends" builds a bigger interface from a smaller one (inheritance).
interface Dog extends Animal {
breed: string; // Dog has name + speak + feed AND breed
}
// You can extend MORE than one at a time:
interface Timestamped { createdAt: Date; }
interface Post extends User, Timestamped {
title: string; // Post has every User + Timestamped field, plus title
}Run this to see an object that satisfies an extended interface (with a method) behaving at runtime:
Worked example: methods, extends & index signatures
A Dog satisfies Animal + its own fields; an env map uses an index signature.
// interface Animal { name: string; speak(): string; }
// interface Dog extends Animal { breed: string; }
//
// A Dog object must supply EVERY Animal member plus its own.
const rex = {
name: "Rex",
breed: "Beagle",
speak() { return this.name + " says Woof!"; }, // satisfies speak(): string
};
console.log(rex.name); // Rex
console.log(rex.breed); // Beagle
console.log(rex.speak()); // Rex says Woof!
// Index signature in action: a dictionary of string -> string.
// interface S
...Now you add a method. A Square extends Shape, so it must provide an area() method. Fill in the blank so it returns the correct area:
🎯 Your turn: implement area()
Complete the method so the Square satisfies its interface.
// 🎯 YOUR TURN — fill in the blanks.
//
// In TypeScript:
// interface Shape { name: string; area(): number; }
// interface Square extends Shape { side: number; }
//
// A Square must have name, side, AND an area() method.
const square = {
name: "tile",
side: 4,
// 👉 add a method "area" that returns side * side
area() {
return ___; // 👉 multiply this.side by this.side
},
};
console.log(square.name + " area = " + square.area());
// ✅ Expected output:
// tile area = 1
...4. Index Signatures
Sometimes you don't know the property names ahead of time — you just know they'll all be strings mapping to, say, strings. An index signature [key: string]: string describes exactly that: "any string key is allowed, and its value must be a string." It's perfect for dictionaries, config maps, and translation tables.
// An index signature says "any key of this type maps to that value type".
interface StringMap {
[key: string]: string; // any string key -> a string value
}
const env: StringMap = {
NODE_ENV: "production",
API_KEY: "abc123",
PORT: "3000",
};
env.REGION = "eu-west-1"; // ✅ any new string key is allowed
// env.count = 5; // ❌ value must be a string, not a number5. interface vs type — Which to Use?
A type alias (type) gives a name to any type — not just object shapes. That includes unions (string | number), intersections (A & B), literal types, tuples, and function types — things an interface simply can't express. For plain object shapes, both work and read almost identically.
// type alias: a name for ANY type, not just object shapes.
type Point = { x: number; y: number }; // object shape
type ID = string | number; // union — interface CANNOT do this
type Direction = "up" | "down" | "left" | "right"; // literal union
type Callback = (data: string) => void; // function type
// Both can describe an object shape:
interface PointI { x: number; y: number } // same idea, interface style
// Rule of thumb:
// • interface -> object shapes & things classes implement (it can be extended/merged)
// • type -> unions, intersections, primitives, tuples, function types🔎 Deep Dive: the decision in one table
| Can it… | interface | type |
|---|---|---|
| Describe an object shape | ✅ | ✅ |
| Be extended / inherited | ✅ extends | ✅ & |
Express a union (A | B) | ❌ | ✅ |
| Name a primitive / tuple / function | ❌ | ✅ |
| Merge two declarations of the same name | ✅ | ❌ |
Rule of thumb: reach for interface for object shapes and public APIs (it can be extended and merged later). Reach for type when you need a union, an intersection, a primitive alias, a tuple, or a function type. When in doubt for an object shape, either is fine — pick one and stay consistent.
Common Errors (and the fix)
- TS2741: "Property 'email' is missing in type … but required in type 'User'." — You left out a required property. Either add the missing field, or mark it optional with
?in the interface if it really can be absent. - TS2353: "Object literal may only specify known properties, and 'age' does not exist in type 'User'." — This is the excess property check: when you assign an object literal directly, TypeScript flags any extra field. Fix the typo (
{ naem: ... }→name), add the property to the interface, or assign through a variable first if the extra field is intentional. - TS2540: "Cannot assign to 'id' because it is a read-only property." — You tried to change a
readonlyfield after creation. Set it once when the object is built, or dropreadonlyif it really needs to change. - TS2551 / TS2339: "Property 'bio' does not exist…" — You read a property the interface never declared. Add it to the interface (optional with
?if appropriate), or check your spelling.
Pro Tips
- 💡 Name interfaces like nouns:
User,OrderLine,Config— they describe a thing. - 💡 Prefer
readonlyfor ids and keys so an accidental reassignment is caught by the compiler. - 💡 Don't over-use optional (
?): if almost every field is optional, you probably need two interfaces (e.g. one for creating, one for the saved record). - 💡 Compose with
extendsinstead of copy-pasting fields between interfaces.
📋 Quick Reference
| Pattern | Syntax |
|---|---|
| Interface | interface User { name: string } |
| Optional property | bio?: string |
| Readonly property | readonly id: number |
| Method signature | speak(): string |
| Extend | interface Dog extends Animal {} |
| Index signature | [key: string]: string |
| Type alias / union | type ID = string | number |
| Intersection | type C = A & B |
Frequently Asked Questions
Q: Does an interface exist when my code runs?
No. Interfaces (and type aliases) are erased during compilation — they're purely a compile-time contract. That's why the runnable demos here are plain JavaScript: the type checking happens in your editor and build step, not at runtime.
Q: When should I use interface vs type?
Use interface for object shapes and anything a class will implement — it can be extended and merged. Use type when you need a union, intersection, primitive alias, tuple, or function type. For a plain object shape either is fine; just be consistent.
Q: What's the difference between ? and readonly?
? controls whether a property must exist (optional vs required). readonly controls whether it can change after it's set. They're independent — a property can be both, neither, or either.
Q: Why did TypeScript reject an extra property it could have ignored?
That's the excess property check (TS2353). When you assign an object literal directly to a typed target, TypeScript flags unknown properties to catch typos. Assign through an intermediate variable, or add the property to the interface, if the extra field is intentional.
Mini-Challenge: a typed Product
No blanks this time — just a brief and an outline. Build an object that satisfies the Product interface (a readonly field, an optional field, and a method), then print its description. Check your output against the example in the comments.
🎯 Mini-Challenge: build a Product
Create the object and its describe() method yourself, then run it.
// 🎯 MINI-CHALLENGE: a typed Product
//
// In TypeScript you'd write this interface (you don't have to retype it):
// interface Product {
// readonly sku: string; // unique code, never changes
// name: string;
// price: number;
// onSale?: boolean; // optional
// describe(): string; // method returning a string
// }
//
// 1. Create an object "product" with: sku, name, price (a number),
// and a describe() method.
// 2. describe() should return: name + " —
...🎉 Lesson Complete
- ✅ An
interfacedescribes the shape an object must have - ✅
?makes a property optional;readonlylocks it after creation - ✅ Method signatures (
speak(): string) require objects to provide functions - ✅
extendscomposes a bigger interface from smaller ones - ✅ Index signatures (
[key: string]: string) model flexible dictionaries - ✅ Use
typefor unions, intersections, and primitives;interfacefor object shapes - ✅ Next lesson: Functions and Generics — type your functions and write reusable, flexible code
Sign up for free to track which lessons you've completed and get learning reminders.