Repository navigation
Class inheritance should not bypass generic parameter contravarianceΒ #53798
Description
Activity
- changed the title
[-]Class inheritance should not bypass generic paramater contravariance[/-][+]Class inheritance should not bypass generic parameter contravariance[/+]on Apr 16, 2023 I believe this is essentially the same problem described at #48240 (comment).
In your example the generic parameter
Tinclass Comparator<in T>is still NOT really contravariant even you use theinannotation, because ts will use structural relations when comparing between different generic types (ComparatorvsDogComparator), andTwill be treated as bivariant.Reacted by Andrii DieievOkay I understand the problem.
But in any case, when a parameter is marked with the
inkeyword, then it should be contravariant, not "contravariant but not really". For me this feature is unfinished.While Iβm inclined to agree this is surprising, itβs also a fundamental design limitation because the type system is primarily structural. If contravariance isnβt witnessed in the structure of the type then youβre going to lose that information when using a type identity that doesnβt include the
inhint.From what I understand variance annotations were added more for the benefit of the implementer than the consumer, for exactly this reason. So
in Tshould be read as βT is at least contravariant, but may be more permissiveβ, because thatβs how the compiler will read it too.Reacted by whzx5bybReacted by FeavyPut another way: If you write
in T, youβre merely asserting thatFoo<T>is assignable toFoo<U>whereUis a subtype ofT, and this is what the compiler will check for when you write the type. If the actual definition of the type says the type parameter is bivariant instead of only contra-, the constraint is still met.RyanCavanaugh commented
on Apr 17, 2023 MemberMore actionsThis is the intentional behavior. Variance annotations only apply when you're comparing two instantiations of the same type; this is the only place it even makes sense to think about doing it in the first place.
POLA is ungeneralizable, but to me this isn't surprising at all. TypeScript is a structural type system and
DogComparator->Comparator<Animal>is a structural operation (since it's not guaranteed to be a correct operation to upcastDogComparatortoComparator<Dog>), and the structural comparison succeeds on account of method parameter bivariance. That's what should happen, and that's what does happen.The only plausible diff I could imagine here would be erroring on the
inannotation since it only subsets the actual variance, but it's unclear how that's a real improvement. Variance annotations are an advanced feature and it's assumed you understand what you are doing and what the trade-offs are.- addedWorking as IntendedThe behavior described is the intended behavior; this is not a bugThe behavior described is the intended behavior; this is not a bug
on Apr 17, 2023 Well, then indeed I have to admit I'm facing the limits of TypeScript's design, which makes things like that being accepted although they don't make any sense.
Anyway, thank you all for your answers.
POLA is ungeneralizable, but to me this isn't surprising at all.
It's maybe a bit surprising if one is expecting the variance annotation to be part of structure (i.e. being a property of the type parameter
Titself, which seems like a reasonable assumption to me; there are many situations where TS works directly with unbound type parameters), but in reality it's a property of a type identity. So there's some potential for an impedance mismatch there.Reacted by Feavy and Ryan CavanaughBy the way I tried the code snippet given in the Why are function parameters bivariant? FAQ section and it actually gives a compile error (which is very welcome π) playground example
Is this section outdated?
Yeah, I've brought it up before that that section is outdated. π It used to be that all function types were bivariant but
strictFunctionTypeslimits it to only methods. The exclusion for methods is very intentional:https://www.typescriptlang.org/docs/handbook/release-notes/typescript-2-6.html#strict-function-types
The stricter checking applies to all function types, except those originating in method or constructor declarations. Methods are excluded specifically to ensure generic classes and interfaces (such as
Array<T>) continue to mostly relate covariantly.If methods were strictly checked,
Array<string>wouldn't be assignable toArray<string | number>because it has its type parameter in contravariant positions... which could be argued to be a good thing (arrays are, in fact, mutable), but the reality is it would break a ton of actual code--including even innocent cases likeconst x: Array<string | number> = [ "foo", "bar" ]--so that wasn't done.But of course this implies that if you really do want the stricter checking for your methods, you have the option of using an interface with function-typed properties instead of methods.
Alright thank you! It is clearer for me now.
Reacted by Bruce Pascoetypescript-bot commented
on Apr 20, 2023 ContributorMore actionsThis issue has been marked 'Working as Intended' and has seen no recent activity. It has been automatically closed for house-keeping purposes.
- locked as resolved and limited conversation to collaborators
on Oct 22, 2025
Bug Report
π Search Terms
contravariance bypass inheritance variance annotation
π Version & Regression Information
β― Playground Link
Playground link with relevant code
π» Code
In this example we use contravariance to make
Comparator<Animal>assignable toComparator<Dog>: something than can compare animals can compare dogs.π Actual behavior
The following line is accepted by the compiler:
I think the problem is that as
DogComparatoris a subclass ofComparator, the compiler only checks for(a: Dog, b: Dog) => booleanto be assignable to(a: Animal, b: Animal) => void, regardless of contravariance annotation put on the generic type.π Expected behavior
The nok line above should give a compilation error as contravariance on
Comparator's generic parameter should preventDogComparator(which extendsComparator<Dog>) from being treated as aComparator<Animal>.Even if
(a: Dog, b: Dog) => booleanis assignable to(a: Animal, b: Animal) => void, there should be a second contraint to check ifAnimalis a subtype ofDog(which will give the error) because of contravariance on the generic parameter.For information this is the case in Kotlin: see this playground sample
Thanks π