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
Type safety lost when generic type used as method parameter #57071
A method and a function property of the same type behave differently.
Methods are always bivariant in their argument, while function properties are contravariant in their argument under strictFunctionTypes. More discussion here.
Still, the strictFunctionTypes option excludes functions created as method declarations, thus making this example still compile with this option turned on.
That being said, yes, introducing explicit type-safe covariant and contravariant generics would require major changes to existing type definitions, but many (strict-typed) languages manage to do that with keywords like in and out (C# or Kotlin). It'd be also possible to introduce something like wildcard types in Java, where instead you have to specify the variance when using the types by using ? extends T for covariance and ? super T for contravariance
Still, the strictFunctionTypes option excludes functions created as method declarations, thus making this example still compile with this option turned on.
Yes, that’s specifically called out in the FAQ excerpt I quoted. From the PR linked there, that was an intentional trade-off:
Methods are excluded specifically to ensure generic classes and interfaces (such as Array<T>) continue to mostly relate covariantly. The impact of strictly checking methods would be a much bigger breaking change as a large number of generic types would become invariant (even so, we may continue to explore this stricter mode).
TypeScript actually does have variance annotations, but they’re only honored in nominal type checks (i.e. between two instantiations of the same generic), and have no effect on structural comparisons.
Readonly is not strict enough to prevent a.setValue(new B()). Something like DeepReadonly, DeepFrozen, or maybe a pure keyword on the function is required.
I.e., this would be nice
letaClass: DeepReadonly<MyClass<A>>=bClass;aClass.setValue(newA());// error, mutations not only allowed under DeepReadonly
Craig P Hicks (@craigphicks) I don't think Readonly is really for that. As docs say, “[Readonly] constructs a type with all properties of Type set to readonly, meaning the properties of the constructed type cannot be reassigned”, so it only makes all the properties readonly. Making a method inaccessible wouldn't really make sense.
Bruce Pascoe (@fatcerberus) Nice to see Typescript actually has variance annotations, didn't find anything about it before. Shame the default is bivariant and that there's no way to enable strict method variance checks, as far as I know. It would be useful to ensure full type safety of generics.
Also, I think it's not possible to use them as a variable type? Like this:
It could work a bit like wildcard generics in Java, by locking the ability to perform certain things that could break type safety. Unless it would be possible using some utility type? TypeScript allows lots of magic with it, but can't think of a way that would make that possible
Also, from what I can see, there isn't a way to explicitly state bivariance in a way that would make the compiler ensure the type is both covariant and contravariant?
Craig P Hicks (@craigphicks) I don't think Readonly is really for that. As docs say, “[Readonly] constructs a type with all properties of Type set to readonly, meaning the properties of the constructed type cannot be reassigned”, so it only makes all the properties readonly. Making a method inaccessible wouldn't really make sense.
Exactly - Readonly is really [not] for that., that's my point.
In the case you only want to read the A-ness of b, you should have a way to do so (and TypeScript currently offers that, as shown in your example) . It would be nice to have a way to read the A-ness of b without risking the kind of error you demonstrate in your example.
However, TypeScript can't make your example an error, because TypeScript puts priority on allowing reading the A-ness of B, and TypeScript has no way to prevent a.setValue being called.
If you proposed a way to prevent a.setValue being called - say
let aClass: DeeplyImmutable<MyClass<A>> = bClass;
(and also features like declaring Methods and Parameters as mutating or not) then TypeScript could make
let aClass: MyClass<A> = bClass;
an error because there would be an alternative safe way (DeeplyImmutable) to allow reading the A-ness of b.
The default is actually "let the compiler measure the variance automatically". Explicit variance annotations were eventually added because it can sometimes get it wrong. When it's working properly, the measured variance is supposed to match the structural variance. In this case, the type is "correctly" measured as covariant because the type parameter T doesn't appear in any purely contravariant positions (thanks to the method exception).
there isn't a way to explicitly state bivariance
Does it ever actually make sense for a type parameter to be bivariant, assuming it's actually used somewhere in the type?
Does it ever actually make sense for a type parameter to be bivariant, assuming it's actually used somewhere in the type?
That's fair, actually; but at the same time it represents how default bivariance inherently implies type safety problems in most cases—that's why I wish we could make it stricter.
Even --strictFunctionTypes doesn't seem to work as expected:
set(value: T): void; seems to be completely allowed in a type with <out T>
You can assign Type<T> to both Type<Supertype> and Type<Subtype> even when T is covariant or contravariant, without even a compiler warning
As I said, variance is measured - generics are not “bivariant by default” in any meaningful sense. Method parameters are bivariant with respect to assignability of the overall type—which can influence variance measurement, of course.
set(value: T): void; seems to be completely allowed in a type with <out T>
That’s expected - without the annotation the type would be measured as covariant anyway, so the annotation adds no information.
You can assign Type<T> to both Type<Supertype> and Type<Subtype> even when T is covariant or contravariant, without even a compiler warning
That’s expected - without the annotation the type would be measured as covariant anyway, so the annotation adds no information.
But that means you could still do
Activity
fatcerberus commentedon Jan 16, 2024
Methods are intentionally bivariant so this is the same mode of unsoundness as
which is not considered to be a defect.
fatcerberus commentedon Jan 16, 2024
The relevant FAQ entry btw is
TheChilliPL commentedon Jan 16, 2024
Still, the
strictFunctionTypesoption excludes functions created as method declarations, thus making this example still compile with this option turned on.That being said, yes, introducing explicit type-safe covariant and contravariant generics would require major changes to existing type definitions, but many (strict-typed) languages manage to do that with keywords like
inandout(C# or Kotlin). It'd be also possible to introduce something like wildcard types in Java, where instead you have to specify the variance when using the types by using? extends Tfor covariance and? super Tfor contravariancefatcerberus commentedon Jan 16, 2024
Yes, that’s specifically called out in the FAQ excerpt I quoted. From the PR linked there, that was an intentional trade-off:
TypeScript actually does have variance annotations, but they’re only honored in nominal type checks (i.e. between two instantiations of the same generic), and have no effect on structural comparisons.
craigphicks commentedon Jan 16, 2024
Chilli! (@TheChilliPL)
Even if that were illegal - you would still want to be able to pass
MyClass<B>via a parameterReadonly<MyClass<A>>for reading purposes. However inReadonlyis not strict enough to preventa.setValue(new B()). Something likeDeepReadonly,DeepFrozen, or maybe apurekeyword on the function is required.I.e., this would be nice
TheChilliPL commentedon Jan 17, 2024
Craig P Hicks (@craigphicks) I don't think Readonly is really for that. As docs say, “[Readonly] constructs a type with all properties of Type set to readonly, meaning the properties of the constructed type cannot be reassigned”, so it only makes all the properties readonly. Making a method inaccessible wouldn't really make sense.
TheChilliPL commentedon Jan 17, 2024
Bruce Pascoe (@fatcerberus) Nice to see Typescript actually has variance annotations, didn't find anything about it before. Shame the default is bivariant and that there's no way to enable strict method variance checks, as far as I know. It would be useful to ensure full type safety of generics.
Also, I think it's not possible to use them as a variable type? Like this:
It could work a bit like wildcard generics in Java, by locking the ability to perform certain things that could break type safety. Unless it would be possible using some utility type? TypeScript allows lots of magic with it, but can't think of a way that would make that possible
TheChilliPL commentedon Jan 17, 2024
Also, from what I can see, there isn't a way to explicitly state bivariance in a way that would make the compiler ensure the type is both covariant and contravariant?
craigphicks commentedon Jan 17, 2024
Chilli! (@TheChilliPL)
Exactly - Readonly is really [not] for that., that's my point.
In the case you only want to read the A-ness of b, you should have a way to do so (and TypeScript currently offers that, as shown in your example) . It would be nice to have a way to read the A-ness of b without risking the kind of error you demonstrate in your example.
However, TypeScript can't make your example an error, because TypeScript puts priority on allowing reading the A-ness of B, and TypeScript has no way to prevent
a.setValuebeing called.If you proposed a way to prevent
a.setValuebeing called - say(and also features like declaring Methods and Parameters as mutating or not) then TypeScript could make
an error because there would be an alternative safe way (
DeeplyImmutable) to allow reading the A-ness of b.fatcerberus commentedon Jan 17, 2024
The default is actually "let the compiler measure the variance automatically". Explicit variance annotations were eventually added because it can sometimes get it wrong. When it's working properly, the measured variance is supposed to match the structural variance. In this case, the type is "correctly" measured as covariant because the type parameter
Tdoesn't appear in any purely contravariant positions (thanks to the method exception).Does it ever actually make sense for a type parameter to be bivariant, assuming it's actually used somewhere in the type?
ritschwumm commentedon Jan 17, 2024
Bruce Pascoe (@fatcerberus) i guess phantom types are the only thing where bivariance makes any sense...
TheChilliPL commentedon Jan 17, 2024
That's fair, actually; but at the same time it represents how default bivariance inherently implies type safety problems in most cases—that's why I wish we could make it stricter.
Even
--strictFunctionTypesdoesn't seem to work as expected:set(value: T): void;seems to be completely allowed in a type with<out T>Type<T>to bothType<Supertype>andType<Subtype>even when T is covariant or contravariant, without even a compiler warningSee playground
fatcerberus commentedon Jan 17, 2024
As I said, variance is measured - generics are not “bivariant by default” in any meaningful sense. Method parameters are bivariant with respect to assignability of the overall type—which can influence variance measurement, of course.
That’s expected - without the annotation the type would be measured as covariant anyway, so the annotation adds no information.
Actually that’s just because all of those are empty types. Make them structurally different and you’ll find the variance annotations work as expected. This is just a reality of structural typing. See https://github.com/microsoft/TypeScript/wiki/FAQ#why-do-these-empty-classes-behave-strangely
TheChilliPL commentedon Jan 18, 2024
breaking type-safety again, despite
--strictFunctionTypesbeing on—see Playgroundfatcerberus commentedon Jan 18, 2024
Yes, you've annotated it covariant (
out), and TS doesn't object because that's what it would measure anyway. The type is properly invariant (in out).TheChilliPL commentedon Jan 20, 2024
Could I make TypeScript then disallow using a covariant generic in method parameters?
fatcerberus commentedon Jan 20, 2024
That's more or less just
strictMethodTypesand it's already been explained above why that doesn't existtypescript-bot commentedon Jan 23, 2024
This issue has been marked as "Question" and has seen no recent activity. It has been automatically closed for house-keeping purposes.