11

I have an interface which is extending another one. Now I am trying to put a variable of the superinterface type into a function that needs a subinterface type parameter. This is not possible as the superinterface typed variable is missing certain attributes.

It's a little hard to explain, so I've created an example with an Animal interface, which is extended by a Horse interface:

interface Animal {
    age:number;
    //... 100 other things
}

interface Horse extends Animal {
    speed:number;
}

Now I have a module with one private function (rideHorse), which only accepts a Horse as parameter. And a public function (rideAnimal), accepting all Animals like below:

module AnimalModule {
    function rideHorse(horse:Horse) {
        alert(horse.speed);
    }

    export function rideAnimal(animal:Animal) {
        animal.speed = 10; // Error: Animal cannot have speed
        rideHorse(animal); // Error: 'animal' needs speed
    }
}

// Calling the rideAnimal function
var myAnimal:Animal = {
    age: 10
};
AnimalModule.rideAnimal(myAnimal);

As you can see this is not working because the animal parameter of rideAnimal doesn't have speed. So my question is: How can I cast my animal into a Horse and add speed manually inside the rideAnimal function, so the errors will disappear?

Duncan Lukkenaer
  • 12,050
  • 13
  • 64
  • 97
  • I don't get it, below in the comments you write that all animals can be considered rideable and that they thus all needs speed. So why don't you just define speed in IAnimal? – Alex Apr 23 '16 at 13:31
  • @Alex The `Animal` interface is already defined and cannot be changed, but my question isn't about that. I just want to know if it's possible to cast a superinterface to a subinterface. – Duncan Lukkenaer Apr 23 '16 at 14:10

4 Answers4

17

You can cast your Animal into a Horse like this.

function rideAnimal(animal:Animal) {
    var horse = animal as Horse;
    horse.speed = 10;
    rideHorse(horse);
}
Valéry
  • 4,574
  • 1
  • 14
  • 25
  • This seems to be the simplest solution to achieve what I want. Although there will now be no warning if you don't add `speed` to the `horse`, so `speed` can be `undefined`. But I suppose that's a small price to pay. – Duncan Lukkenaer Apr 28 '16 at 10:04
  • 2
    If you want more safety, use a conversion function `toHorse` that takes an `Animal`, casts it to a `Horse` and returns it after adding a `speed`. – Valéry Apr 28 '16 at 18:13
  • Or add conditional checks to check that the animal is a horse (throw otherwise). I think that's the best way. – trusktr Jun 11 '20 at 19:17
5

Yes, you can use user defined type guards to handle this kind of custom type evaluation:

interface Animal
{
    age: number;
    //... 100 other things
}

interface Horse extends Animal
{
    speed: number;
}

module AnimalModule
{
    function rideHorse(horse: Horse)
    {
        alert(horse.speed);
    }

    function isHorse(a: Animal): a is Horse
    {
        return "speed" in a;
    }

    export function rideAnimal(animal: Animal)
    {
        if (isHorse(animal))
        {
            animal.speed = 10; // Ok
            rideHorse(animal); // Ok
        } else
            throw new Error("You can only ride horses")
    }
}

If Horse and Animal were classes, you could have just done if (animal instanceof Horse) instead of using the custom type guard isHorse

Alex
  • 14,104
  • 11
  • 54
  • 77
0

You can do whatever you want, since typescript is just javascript in the end. But then you loose the static typing benefits... For example, you ca write ( animal).speed = 12, but it breaks compile time checks...

In an object oriented point of view, you should not be able to ride an animal with no speed.

You should add an intermediate interface "Irideable" extending animal and inherited by horse.

Regis Portalez
  • 4,675
  • 1
  • 29
  • 41
  • I know it shouldn't be possible to ride an `Animal` without `speed`, that's why I want to add `speed` to each `animal` object inside `rideAnimal`. – Duncan Lukkenaer Apr 23 '16 at 11:12
  • Can you ride a worm? Your question makes no sense. You can still cast your animal to any and add the speed property to it. But it's ugly design – Regis Portalez Apr 23 '16 at 12:12
  • I get your point, but in my case you can assume that the `animal` is ridable, but is just missing the `speed` property. Sorry about my example not accurately reflecting the real world. – Duncan Lukkenaer Apr 23 '16 at 12:47
0

I would expect your rideAnimal function to look like

export function rideAnimal<T extends Animal>(animal:T) {
    animal.speed = 10; // Error: Animal cannot have speed
    if (animal instanceof Horse) {
        rideHorse(animal as any as Horse); // Error: 'animal' needs speed
    }
}

This is actually a pretty common pattern when working with Abstract Syntax Trees and you are given a reference of a generic node, and need to figure out what type it really is.

A full example would be:

class A {}

class B extends A {
  doSomething() {}
}

function test(a:A) {
  if (a instanceof B) {
    a.doSomething();
  }
}
ArcSine
  • 648
  • 6
  • 14
  • Although in my situation every `Animal` should become a `Horse`, so wouldn't such an if-statement be unnecessary? Also, this doesn't get rid of the errors. – Duncan Lukkenaer Apr 23 '16 at 11:40
  • Updated the code to use generics to hopefully be a little more robust, but honestly if you are looking for certain properties you might want to consider structural typing. E.g. only call the rideHorse method if the animal has a speed property vs what class the animal belongs to. – ArcSine Apr 23 '16 at 11:46