Execute Program

Everyday TypeScript: Extending Types

Welcome to the Extending Types lesson!

This lesson is shown as static text below. However, it's designed to be used interactively. Click the button below to start!

  • An earlier lesson showed what happens when two interface declarations have the same name. We get "declaration merging": the final interface has all of the properties from both interface declarations.

  • Declaration merging is useful, but has an important gotcha. The problem is that it overwrites the original interface, leaving us no way to use both interfaces.

  • More often, we want to say "this new interface is like the original, but has some additional properties". We want to be able to use both interfaces in different situations: the original "smaller" interface, and the new "larger" interface that extends it.

  • Fortunately, we can do exactly that with an interface that extends another interface. The extending interface automatically has all of the properties from the extended interface.

  • >
    interface CanBeAdmin {
    admin: boolean
    }

    interface User extends CanBeAdmin {
    name: string
    }

    const user: User = {name: 'Amir', admin: true};
    user;
    Result:
    {name: 'Amir', admin: true}Pass Icon
  • Interfaces can also extend object types defined using the type keyword.

  • >
    type CanBeLocked = {
    locked: boolean
    };

    interface User extends CanBeLocked {
    name: string
    }

    const user: User = {name: 'Amir', locked: false};
    user;
    Result:
    {name: 'Amir', locked: false}Pass Icon
  • Interfaces can even extend classes. That may seem strange at first: classes can have methods and can be instantiated. Interfaces can't do either of those, so what does it mean for an interface to extend a class?

  • When an interface extends a class, it means "objects with this interface type must have the properties that an instance of the class would have." For example, if an interface extends a class that has a canWalk(): boolean method, then an object with that interface must have a canWalk: () => boolean property. The same applies to the class's fields: an interface that extends the class must have each of the class's fields.

  • >
    class Animal {
    legs: number;

    constructor(legs: number) {
    this.legs = legs;
    }

    canWalk() {
    return this.legs > 0;
    }
    }

    // Pets are animals, but they also have names.
    interface Pet extends Animal {
    name: string
    }

    const msFluff: Pet = {
    legs: 4,
    canWalk: () => true,
    name: 'Ms. Fluff',
    };
  • Note: this code example reuses elements (variables, etc.) defined in earlier examples.
    >
    [msFluff.name, msFluff.canWalk()];
    Result:
    ['Ms. Fluff', true]Pass Icon
  • It's important to note that Ms. Fluff is not an instance of Animal! She has the same interface as Animal, meaning that she has the same properties. But msFluff is a Pet, which isn't an instance of any class at all.

  • Note: this code example reuses elements (variables, etc.) defined in earlier examples.
    >
    msFluff instanceof Animal;
    Result:
    falsePass Icon
  • Our Pet type is a combination of the original Animal class's fields and methods, plus the extra name property. But it's enforced as if all of its properties were declared in a single interface declaration. If we omit any of the original class's properties, including its methods, then we'll get a type error.

  • Note: this code example reuses elements (variables, etc.) defined in earlier examples.
    >
    interface Pet extends Animal {
    name: string
    }

    const msFluffMissingLegs: Pet = {
    canWalk: () => true,
    name: 'Ms. Fluff',
    };
    Result:
    type error: Property 'legs' is missing in type '{ canWalk: () => true; name: string; }' but required in type 'Pet'.Pass Icon
  • Here's a code problem:

    We have separate Cat and CanBeVaccinated interfaces, but they're not currently related. Modify the Cat interface to extend the CanBeVaccinated interface, which will give it all of CanBeVaccinated's properties. (You won't need to modify the interface's properties.)

    interface CanBeVaccinated {
    vaccinated: boolean
    }
    interface Cat extends CanBeVaccinated {
    name: string
    }
    const msFluff: Cat = {name: 'Ms. Fluff', vaccinated: true};
    msFluff;
    Goal:
    {name: 'Ms. Fluff', vaccinated: true}
    Yours:
    {name: 'Ms. Fluff', vaccinated: true}Pass Icon
  • Finally, a note about where we can use extends. In a previous lesson, we saw that classes can extend classes. In this lesson, we saw that interfaces can extend classes, object types, and other interfaces.

  • However, types defined with the type keyword can't extend anything. There's simply no syntax for it: we can't say type Cat extends Animal = { ... }.

  • With type, we can get a similar effect by intersecting types using the & operator. Unfortunately, that produces worse type error messages, which we'll see in a future lesson.