TypeScript: subtyping and covariant argument types
Asked Answered
E

1

8

Common sense suggests that subtyping should be covariant with respect to return type but contravariant with respect to argument types. So, the following should be rejected, because of the strictly covariant argument type of E.f:

interface C {
   f (o: C): void
}

interface D extends C {
   g (): void // give D an extra service
}

class E implements C {
   // implement f with a version which makes stronger assumptions
   f (o: D): void {
      o.g() // rely on the extra service promised by D
   }
}

// E doesn't provide the service required, but E.f will accept
// an E argument as long as I invoke it via C.
var c: C = new E()
console.log('Try this: ' + c.f(c))

Indeed, running the program prints

Uncaught TypeError: o.g is not a function

So: (1) what's the rationale here (presumably there is one, however unsatisfying and JavaScripty); and (2) is there any practical reason why the compiler can't omit a warning in this situation?

Endorsed answered 19/9, 2016 at 8:51 Comment(4)
I think this would make a good issue for typescripts github page. :)Misspeak
Check also this issue for Typescript function bivariance.Asthenic
I suppose "bivariance" (related via < or >) is better than no typing at all, but it's still quite dangerous. Iinvariance would be safer, but I guess that interacts poorly with other requirements inherited from JS :). Thanks, that's indeed useful and relevant.Endorsed
Ok, I see: the problem is with mutable data structures. In a pure language, my "naive" expectation (contravariance w.r.t. argument types) is the only sane option, but with mutability it depends on whether you're reading from or writing to the structure. (I remember this issue now from C++.)Endorsed
E
7

As per krontogiannis' comment above, when comparing function types, one can be a subtype of the other either because a source parameter type is assignable to the corresponding target parameter type, or because the corresponding target parameter type is assignable to the source parameter type. In the language specification this is called function parameter bivariance.

The reason for permitting bivariant argument types, as opposed to the "naive" expectation of contravariance, is that objects are mutable. In a pure language contravariance is the only sane option, but with mutable objects whether covariance or contravariance makes sense depends on whether you're reading from or writing to the structure. Since there's no way (currently) to express this distinction in the type system, bivariance is a reasonable (albeit unsound) compromise.

Endorsed answered 19/12, 2016 at 19:20 Comment(1)
See also this recent change to the language.Endorsed

© 2022 - 2024 — McMap. All rights reserved.