Contravariance doesn't seem to be inferred by TypeScript. The example below highlights this inconsistency:
class Base { base = "I'm base" }
class Der extends Base { der = "I'm der" }
interface Getter<E> { get(): E }
interface Setter<E> { set(value: E): void }
type Test1 = Getter<Der> extends Getter<Base> ? 'Yes' : 'No' // "Yes"
type Test2 = Getter<Base> extends Getter<Der> ? 'Yes' : 'No' // "No"
type Test3 = Setter<Der> extends Setter<Base> ? 'Yes' : 'No' // "Yes"
type Test4 = Setter<Base> extends Setter<Der> ? 'Yes' : 'No' // "Yes"
The unexpected behavior arises with Test3
, as it allows a scenario like:
const setBase = (setter: Setter<Base>) => setter.set(new Base())
const derSetter = {
set: (thing: Der) => console.log(thing.der.toLowerCase())
}
setBase(derSetter); // Cannot read property 'toLowerCase' of undefined!
In my specific use case involving reading values from HTML elements, there's an interface named Property
which resembles Setter
but uses get
instead of set
:
interface Property<E extends Element> {
get(element: E): string
}
// More content...
Case 1 works fine, while Case 2 encounters issues due to mismatched types:
// Case 1
new ElementProperty(document.head, {
get(element: Element) { return element.outerHTML.toLowerCase(); }
})
// Case 2
const element: Element = document.body;
new ElementProperty(element, {
get(element: HTMLLinkElement) { return element.href.toLowerCase(); }
})
If a method that returns type E is introduced in the Property
interface and implemented, TypeScript correctly detects errors related to Case 1 but not Case 2.
To address this issue, is there a workaround available in TypeScript to prevent scenarios like Case 2? Even if variance is sacrificed, a solution similar to the following could work:
class ElementProperty<E extends Element, P === Property<E>> { ... }