2

I got some typescript interface, abstract class and implementing sub-classes:

// Animal classes

abstract class Animal {
    abstract sound(): string;
    constructor(public name: string) {
    }
    eat(food: string): string {
        return "I eat this now: " + food;
    }
}

class Snake extends Animal{
    constructor() {
        super("Snake");
    }
    sound() {
        return "Sssssss";
    }
}

class Owl extends Animal{
    constructor() {
        super("Owl"); 
    }
    sound() {
        return "Hu-huu";
    }

    // Owl can also fly!
    fly() {
        return "I can flyyyy";
    }
}

// Box classes

interface BoxInterface {
    animal: Animal;
}

class Box implements BoxInterface {

    animal: Animal;

    constructor(animal: Animal) {
        this.animal = animal;
    }

}

As you can see the idea is that we have a Box and some kind of Animal in the box - in our example it can be Snake or Owl.

Now we can create Box with Owl inside.

let box = new Box( new Owl() );

And now the problem - using any method declared in superclass is completely fine:

box.animal.sound(); // this is fine

but as you can see Owl have additional function fly() and because fly is not declared in Animal it throw:

box.animal.fly(); // Property 'fly' does not exist on type 'Animal'.

Also the same happens when creating normal variable:

let animal:Animal;
animal = new Owl();
animal.fly();

As addition Animal class do not have to be abstract, it can be normal class or interface - result will be the same.

My question is: why typescript throw it if my class is superset of other class. I think the main idea of interfaces and typing is guaranteeing that object has some properties like eat() or sound() in this example.

Im very new in typescript so it can be that I missed something, anyway how I can achieve that some variable must be some type but allowing additional methods in subclasses?

Griva
  • 1,618
  • 20
  • 37

5 Answers5

4

Because typescript will not perform inference type for animal: Animal;

As you defined animal as an Animal, so only methods and fields defined in Animal will be available.

It is the way which strong typing works.

If you declare animal as :

animal

or

animal : any

You will be able to invoke any method on it but you lose the type checking.

As workaround, you could use a cast to manipulate a Owl if the animal IS a Owl.

Type assertions are a way to tell the compiler “trust me, I know what I’m doing.” A type assertion is like a type cast in other languages, but performs no special checking or restructuring of data. It has no runtime impact, and is used purely by the compiler. TypeScript assumes that you, the programmer, have performed any special checks that you need.

if (box.animal instanceof Owl){
    (box.animal as Owl).fly
}

But a better way would be to have a generic Box :

class Box<T extends Animal> implements BoxInterface {

    animal: T

    constructor(animal: T) {
        this.animal = animal
    }

}

Now you can write :

let box = new Box<Owl>(new Owl());
box.animal.sound()
box.animal.fly()

In any case, as IMSoP very well said : you have to know at some point that what you have is an Owl if you want to apply method specific to Owl.

davidxxx
  • 125,838
  • 23
  • 214
  • 215
  • but the idea is i only know this object will be of type Animal but i have no idea it is owl or snake. I am only interested to have shared methods like eat so I don't get error but it is ok if owl have some other additonal properties. isnt that point of this concept? – Griva Dec 14 '17 at 16:29
  • I understand. With generic you can get this behavior. I updated. – davidxxx Dec 14 '17 at 16:37
  • If I get these objects from some factory/builder for example `builder.createRandomAnimal();` can I use generic? It is like i later determine this is owl or snake and i run some additional methods – Griva Dec 14 '17 at 16:40
  • 2
    @Griva You have to know at some point that what you have is an `Owl`, otherwise you don't know it can `fly`. So either you have a special `Box` which can only contain `Owl`s (a `Box`), or you check the object and use a "type assertion" as explained in this answer. The alternative is to say "I don't care about type checking, just try and give a runtime error if it doesn't work"; in which case, TypeScript is not the language you are looking for. – IMSoP Dec 14 '17 at 16:44
  • 1
    Upvoted for the Special Owl Box and "You have to know at some point that what you have in an `Owl`". That is key. I'd even add that into the answer. I think it's a hurdle that new OOP programmers don't quite get and expect the language to just infer it. – zero298 Dec 14 '17 at 17:10
  • @zero298 Thanks for the feedback. I am not the author of the little sentence but I also like it. I added it. – davidxxx Dec 14 '17 at 17:17
2

Because Boxes are only guaranteed to have Animals in them. Not all Animals can fly().

You could cast (type assert) Box's Animal to an Owl, and then have it fly:

(box.animal as Owl).fly()
zero298
  • 25,467
  • 10
  • 75
  • 100
1

You are right that a base class contract guarantees a minimum set of methods known to be available on all its sub-types.

However, TypeScript is enforcing an additional constraint here, that you should only invoke methods that you know are available on the object that you have.

In this case, all you know while you are writing the code box.animal.fly(); is that you have an Animal; therefore, you should only call methods that all animals have.

Consider what would happen if this check was not there:

let animal:Animal;
animal = new Snake();
animal.fly();

You would get some kind of error at runtime. The idea of the type checker is to spot this possibility for you, and make you write code which is "safe" in this sense.

IMSoP
  • 89,526
  • 13
  • 117
  • 169
0

This is a OOP matter. When you declare a variable of a certain type and assign a value to it the value (sort of) has 2 types: a runtime type and a static (compile time) type. The type considered by the compiler is the one declared for that variable (which is animal, not Owl and so can't have a method fly()). This has implication on overriden methods (they could be resolved by static or dynamic information, depending on language specification). For more info look at

What is the difference between statically typed and dynamically typed languages? https://en.wikipedia.org/wiki/Multiple_dispatch

ilmirons
  • 624
  • 6
  • 16
0

All other answers are valid. To your problem, you are best off using generics

class Box<T extends Animal> {
  constructor(public animal: T) { }
}

const box = new Box(new Owl())
box.animal.fly()
unional
  • 14,651
  • 5
  • 32
  • 56