Hacker News new | ask | show | jobs
by globuous 1156 days ago
Yeah, there are some weird stuff in typescript, for instance, this typechecks

    class Animal {}

    class Dog extends Animal {
        woof() {}
    }

    class Cat extends Animal {
        meow() {}
    }

    let append_animals = (animals: Animal[], animal: Animal) => animals.push(animal)

    let dogs = [new Dog()]
    append_animals(dogs, new Cat())

    dogs.map(dog => dog.woof())

Which if you evaluate, you'll obviously get:

    Uncaught TypeError: dog.woof is not a function
Whereas Mypy won't typecheck the equivalent Python code:

    class Animal:
        pass

    class Dog(Animal):
        pass


    def append_animals(animals: List[Animal], animal: Animal) -> None:
        animals.append(animal)


    dogs = [Dog()]
    append_animals(dogs, Dog())

It'll throw with:

    $ mypy types.py 
    types.py:16: error: Argument 1 to "append_animals" has incompatible type "List[Dog]"; expected "List[Animal]"
    types.py:16: note: "List" is invariant -- see https://mypy.readthedocs.io/en/stable/common_issues.html#variance
    types.py:16: note: Consider using "Sequence" instead, which is covariant
2 comments

The problem with your TS code is that it's using covariance on a mutable generic type, which is unsafe and strict type systems would've forbidden that.

To expand: TS treats `Dog[]` as a subtype of `Animal[]` because `Dog` is a subtype of `Animal`... that work if you only read values from the array... but trying to change the array, you run into trouble. Some languages let you declare covariance (reading ) and contravariance explicitly to address this issue. To my limited knowledge of TS, that's not possible in TS (as it tries to keep things simple and compatible with JS, probably).

The answers in this[1] SO question explain these concepts better than I could.

[1] https://stackoverflow.com/questions/27414991/contravariance-...

Why was the `dogs` array initialized as an `Animal[]` type instead of `Dog[]` type which would forbid the addition of a `Cat` type?

Why would you be able to map a call to `woof` over an `Animal[]` when `Animal` doesn't implement `woof`? I don't understand how the SO link answers these questions.

> Why was the `dogs` array initialized as an `Animal[]` type instead of `Dog[]`

That might be your confusion: it wasn't. Its type is `Dog[]`.

> which would forbid the addition of a `Cat` type?

Why would that be forbidden? The problematic method is `append_animals`, which only cares that both arguments satisfy `Animal`, which both `Dog` and `Cat` do.

> Why would you be able to map a call to `woof` over an `Animal[]` when `Animal` doesn't implement `woof`

Back to your root confusion, since for all intents and purposes, `dogs.map` thinks it's an array of dogs, it doesn't complain.

If `append_animals` was written like this, things would be fine:

    let append_animals = <T extends Animal>(animals: T[], animal: T) => animals.push(animal)
I see what you're saying. Thanks for taking the time to explain.
From my point of view, this is one of those cases where structural typing just doesn't work all that well when used for OOP.

Typescript does give you a solution to this problem, namely that you use generics to constrain the parameters of your method:

    let append_animals = <T extends Animal>(animals: T[], animal: T) => animals.push(animal)
Your example now gives the expected error.

TypeScript does indeed have its quirks, but most of them do not really matter for real-life purposes or can easily be worked around like in the example above.