Skip to main content
    Courses/TypeScript/Interfaces and Type Aliases

    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

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

    Try it Yourself »
    JavaScript
    // 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 change

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

    Try it Yourself »
    JavaScript
    // 🎯 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.

    Try it Yourself »
    JavaScript
    // 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.

    Try it Yourself »
    JavaScript
    // 🎯 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 number

    5. 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…interfacetype
    Describe an object shape
    Be extended / inheritedextends&
    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 readonly field after creation. Set it once when the object is built, or drop readonly if 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 readonly for 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 extends instead of copy-pasting fields between interfaces.

    📋 Quick Reference

    PatternSyntax
    Interfaceinterface User { name: string }
    Optional propertybio?: string
    Readonly propertyreadonly id: number
    Method signaturespeak(): string
    Extendinterface Dog extends Animal {}
    Index signature[key: string]: string
    Type alias / uniontype ID = string | number
    Intersectiontype 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.

    Try it Yourself »
    JavaScript
    // 🎯 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 interface describes the shape an object must have
    • ? makes a property optional; readonly locks it after creation
    • ✅ Method signatures (speak(): string) require objects to provide functions
    • extends composes a bigger interface from smaller ones
    • ✅ Index signatures ([key: string]: string) model flexible dictionaries
    • ✅ Use type for unions, intersections, and primitives; interface for 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.

    Previous

    Cookie & Privacy Settings

    We use cookies to improve your experience, analyze traffic, and show personalized ads. You can manage your preferences below.

    By clicking "Accept All", you consent to our use of cookies for analytics and personalized advertising. You can customize your preferences or reject non-essential cookies.

    Privacy PolicyTerms of Service