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
I have found these two super useful, but rather unrelated, explanations of why function parameters are bivariant in TypeScript. Would it be helpful to merge them into one?
The first one is that the programmer may know that an object of type T[] will be only read from or only written to, but has no way of communicating that knowledge to TypeScript. Since the methods that write to an array are contravariant in T (T is their argument type) and the methods that read from the array are covariant in T (T is their return type), TypeScript permits either direction even though it cannot statically verify correctness.
The second one is that the programmer may know that the correct type of one argument (which is a function) may depend on the type of another argument; and more specifically, in a common JS pattern, it is the function argument type that varies. Again, there is no way to express it to TypeScript. Rather than giving up completely, TypeScript offers at least partial type safety by asking the programmer to indicate the broadest argument type that the function might accept, which allows it to use bivariance in its type check.
Edit: discussion in #1394 and #10717 is highly relevant.
classAnimal{pet(): void;}classDogextendsAnimal{bark(): void;}classCatextendsAnimal{meow(): void;}functiondoSomethingWithAnimals<TextendsAnimal>(array: T[]): void{// The only place this function can get an instance of T is from the array itself, which is safe to add back to the arrayletanimal: T=array.pop();// The only interface available to interact with T is that of Animalanimal.meow();// compilation erroranimal.pet();// validarray.push(animal);// Safe, the item came from the array}functionaddAnimal<TextendsAnimal,UextendsT>(array: T[],animal: U): void{array.push(animal);// Necessarily safe}constdogs: Dog[]=[newDog()];constcats: Cat[]=[newCat()];letanimals: Animal[]=dogs;// No need for this to ever be allowed.animals=[newDog(),newCat()];// SafedoSomethingWithAnimals(dogs);// ValiddoSomethingWithAnimals(cats);// ValidaddAnimal(dogs,newCat());// Compiler erroraddAnimal(cats,newDog());// Compiler erroraddAnimial(dogs,newDog());// ValidaddAnimal(animals,newDog());// ValidaddAnimal(animals,newCat());// Valid
So with both original arguments no longer valid, what reasons remain for function parameter bivariance (as opposed to contravariance that would ensure type safety)? In my, admittedly very limited, understanding of this issue, silently allowing covariance in function arguments opens a non-trivial hole in type safety.
If you simply make function parameters contravariant, then you lose a critical property of generics: that Foo<T> is necessarily a subtype of Foo<U> if T is a subtype of U. This is both unintuitive and extremely expensive (all "fast" generic checks now become much slower structural expansions).
Fundamentally you can't fix this without explicitly differentiating things like Array<T>#indexOf from Array<T>#push - these functions appear to both take a single T and return a number, but need different semantics applied to them to be sound. This also isn't about ReadOnlyArray vs not -- consider 0-argument sort, for example, which doesn't need variance either way but can't be on ReadOnlyArray.
Ryan Cavanaugh (@RyanCavanaugh) By structural expansions, you mean the type checker would, for each generic class, examine how each of its type variables appears in its methods, and based on that, choose the variance of the class with respect to each type variable? For example, with respect to the type variable that appears only in method return types, the class would be covariant; and with respect to the type variable that appears in both argument and return types, the class would be invariant.
Is this really very slow? It only needds to be done once per class definition. (Edit: it might make it even faster if the programmer can indicate the variance in generic class definition; then the type checker just needs to verify it.)
And I can see that it would cause far too many generic classes to end up invariant; but I suppose it's possible to make the class hierarchy a bit more refined, by adding mix-in classes that behave covariantly because they don't have type variables in method arguments.
Activity
aluanhaddad commentedon Apr 2, 2017
Great idea but I'm not sure that the second example fits anymore given that inspiration for the motivating example is now declared safely https://github.com/Microsoft/TypeScript/blob/master/lib/lib.dom.d.ts#L2671
PyroVortex commentedon Apr 4, 2017
The first one is not really true anymore either:
pkch commentedon Apr 4, 2017
Oh very interesting.
So with both original arguments no longer valid, what reasons remain for function parameter bivariance (as opposed to contravariance that would ensure type safety)? In my, admittedly very limited, understanding of this issue, silently allowing covariance in function arguments opens a non-trivial hole in type safety.
RyanCavanaugh commentedon Apr 4, 2017
If you simply make function parameters contravariant, then you lose a critical property of generics: that
Foo<T>is necessarily a subtype ofFoo<U>ifTis a subtype ofU. This is both unintuitive and extremely expensive (all "fast" generic checks now become much slower structural expansions).Fundamentally you can't fix this without explicitly differentiating things like
Array<T>#indexOffromArray<T>#push- these functions appear to both take a singleTand return a number, but need different semantics applied to them to be sound. This also isn't aboutReadOnlyArrayvs not -- consider 0-argumentsort, for example, which doesn't need variance either way but can't be onReadOnlyArray.pkch commentedon Apr 6, 2017
Ryan Cavanaugh (@RyanCavanaugh) By structural expansions, you mean the type checker would, for each generic class, examine how each of its type variables appears in its methods, and based on that, choose the variance of the class with respect to each type variable? For example, with respect to the type variable that appears only in method return types, the class would be covariant; and with respect to the type variable that appears in both argument and return types, the class would be invariant.
Is this really very slow? It only needds to be done once per class definition. (Edit: it might make it even faster if the programmer can indicate the variance in generic class definition; then the type checker just needs to verify it.)
And I can see that it would cause far too many generic classes to end up invariant; but I suppose it's possible to make the class hierarchy a bit more refined, by adding mix-in classes that behave covariantly because they don't have type variables in method arguments.