You signed in with another tab or window. Reload to refresh your session.You signed out in another tab or window. Reload to refresh your session.You switched accounts on another tab or window. Reload to refresh your session.Dismiss alert
{{ message }}
Repository navigation
Class inheritance should not bypass generic parameter contravarianceΒ #53798
In this example we use contravariance to make Comparator<Animal> assignable to Comparator<Dog> : something than can compare animals can compare dogs.
classAnimal{}classDogextendsAnimal{wouf=true;}classComparator<inT>{publiccompare(a: T,b: T): boolean{returntrue;}}classDogComparatorextendsComparator<Dog>{compareDogs=true;}// [ok] an Animal comparator can compare Dogs, so then it is assignable to Comparator<Dog>constokCompareDogs: Comparator<Dog>=newComparator<Animal>();// [error as expected] a Dog comparator cannot compare Animals as it may require specific props of Dog for comparison constokCompareAnimals: Comparator<Animal>=newComparator<Dog>();// [should be an error] It should be the same here, the fact that DogComparator is a subclass of Comparator<Dog> should not bypass contravariance// DogComparator which extends Comparator<Dog> is still not a subclass of Comparator<Animal> as Animal is not a subclass of DogconstnokCompareAnimals: Comparator<Animal>=newDogComparator();
I think the problem is that as DogComparator is a subclass of Comparator, the compiler only checks for (a: Dog, b: Dog) => boolean to 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 prevent DogComparator (which extends Comparator<Dog>) from being treated as a Comparator<Animal>.
Even if (a: Dog, b: Dog) => boolean is assignable to (a: Animal, b: Animal) => void, there should be a second contraint to check if Animal is a subtype of Dog (which will give the error) because of contravariance on the generic parameter.
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 T in class Comparator<in T> is still NOT really contravariant even you use the in annotation, because ts will use structural relations when comparing between different generic types (Comparator vs DogComparator), and T will be treated as bivariant.
But in any case, when a parameter is marked with the in keyword, 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 in hint.
From what I understand variance annotations were added more for the benefit of the implementer than the consumer, for exactly this reason. So in T should be read as βT is at least contravariant, but may be more permissiveβ, because thatβs how the compiler will read it too.
Put another way: If you write in T, youβre merely asserting that Foo<T> is assignable to Foo<U> where U is a subtype of T, 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.
This 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 upcast DogComparator to Comparator<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 in annotation 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.
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.
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 T itself, 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.
Yeah, I've brought it up before that that section is outdated. π It used to be that all function types were bivariant but strictFunctionTypes limits it to only methods. The exclusion for methods is very intentional:
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 to Array<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 like const 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.
Activity
[-]Class inheritance should not bypass generic paramater contravariance[/-][+]Class inheritance should not bypass generic parameter contravariance[/+]whzx5byb commentedon 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.Feavy commentedon Apr 16, 2023
Okay 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.fatcerberus commentedon Apr 16, 2023
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.fatcerberus commentedon Apr 16, 2023
Put 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 commentedon Apr 17, 2023
This 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.Feavy commentedon 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.
fatcerberus commentedon Apr 17, 2023
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.Feavy commentedon Apr 18, 2023
By 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?
fatcerberus commentedon Apr 18, 2023
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
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.
Feavy commentedon Apr 18, 2023
Alright thank you! It is clearer for me now.
typescript-bot commentedon Apr 20, 2023
This issue has been marked 'Working as Intended' and has seen no recent activity. It has been automatically closed for house-keeping purposes.