Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Docs: function parameter bivariance #14973

Closed
pkch opened this issue Apr 2, 2017 · 5 comments
Closed

Docs: function parameter bivariance #14973

pkch opened this issue Apr 2, 2017 · 5 comments
Labels
Question An issue which isn't directly actionable in code

Comments

@pkch
Copy link

pkch commented Apr 2, 2017

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.

@aluanhaddad
Copy link
Contributor

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
Copy link

The first one is not really true anymore either:

class Animal {
    pet(): void;
}
class Dog extends Animal {
    bark(): void;
}
class Cat extends Animal {
    meow(): void;
}

function doSomethingWithAnimals<T extends Animal>(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 array
    let animal: T = array.pop();
    // The only interface available to interact with T is that of Animal
    animal.meow(); // compilation error
    animal.pet(); // valid
    array.push(animal); // Safe, the item came from the array
}

function addAnimal<T extends Animal, U extends T>(array: T[], animal: U): void {
    array.push(animal); // Necessarily safe
}

const dogs: Dog[] = [new Dog()];
const cats: Cat[] = [new Cat()];
let animals: Animal[] = dogs; // No need for this to ever be allowed.
animals = [new Dog(), new Cat()]; // Safe
doSomethingWithAnimals(dogs); // Valid
doSomethingWithAnimals(cats); // Valid
addAnimal(dogs, new Cat()); // Compiler error
addAnimal(cats, new Dog()); // Compiler error
addAnimial(dogs, new Dog()); // Valid
addAnimal(animals, new Dog()); // Valid
addAnimal(animals, new Cat()); // Valid

@pkch
Copy link
Author

pkch commented 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 RyanCavanaugh added the Question An issue which isn't directly actionable in code label Apr 4, 2017
@RyanCavanaugh
Copy link
Member

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.

@pkch
Copy link
Author

pkch commented Apr 6, 2017

@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.

Sign up for free to subscribe to this conversation on GitHub. Already have an account? Sign in.
Labels
Question An issue which isn't directly actionable in code
Projects
None yet
Development

No branches or pull requests

5 participants