class Dog { name: String }
class Engine { name: String }
function start(ref: Engine) {
if (!(ref instanceof Engine))
throw Error("oops")
}
// Typescript does not complain
start(new Dog())
This sample is due to Typescript doing "structural typing". According to its rules, if Engine and Dog both have the "name" property, then they must have the same behaviour as well. This is also inconsistent with the behaviour of classes in Javascript (e.g. see the behaviour of `instanceof`).The biggest problem is that generics are what they call "bivariant", which is lingo for "completely fucked up". Here's a classic gotcha that caught people by surprise with Java's array since forever:
class Animal { isAnimal: Boolean = true }
class Dog extends Animal { isDog: Boolean = true}
class Cat extends Animal { isCat: Boolean = true }
const dogs: Dog[] = [ new Dog() ]
const animals: Animal[] = dogs
animals.push(new Cat)
for (const dog of dogs) console.log(dog.isDog)
//=> true
//=> undefined
Well, it's unfair to pick on this, since they give a similar example in their own documentation, right? But this extends to inheritance as well, because input parameters in functions are not contravariant in Typescript: abstract class FlatMap<A> {
abstract flatMap<B>(f: (a:A) => FlatMap<B>): FlatMap<B>
}
class Box<A> extends FlatMap<A> {
flatMap<B>(f: (a: A) => Box<B>): Box<B> {
throw new Error("Not implemented!")
}
}
It should be obvious to everybody why this is wrong: class AnotherBox<A> extends FlatMap<A> { ... }
const box: FlatMap<number> = new Box<number>()
box.flatMap(x => new AnotherBox<number>())
In case you're wondering, that interface is the beginning of the Monad pattern and the problem here is that monads are incompatible with each other. So in order to compose instances together, you do rely on the compiler to protect you. Think Promises being combined with Arrays, both of which are monadic types. Not going to work, is it?In less expressive static language lacking higher kinded types, which you need to express such interfaces, there's a current trick that people do to work around it: https://www.cl.cam.ac.uk/~jdy22/papers/lightweight-higher-ki... ; This has been used in Elm and it's being used in Kotlin as well, see for example: https://github.com/FineCinnamon/Katz
But Typescript is on a whole new level of wrong, because you can't trust the compiler for correctness, so you can't trust it for protection, at all. You see, this is not pie in the sky academics, but actual code that can happen in your app, mistakes that the compiler should have prevented you from making. In the above case Typescript really is just documentation and just like documentation in many cases, it can be wrong documentation. To make this even more frustrating, Microsoft's other language, C#, does not have the problems that I enumerated.
And I know what people say - oh, this is still useful. Well, guess what, writing JSDoc + having Google Closure to validate is actually more useful, while not being non-standard ;-)