Skip to content

Commit bf1e2a6

Browse files
Document variance annotations and their limitations (#3218)
1 parent 17e8348 commit bf1e2a6

File tree

1 file changed

+123
-0
lines changed
  • packages/documentation/copy/en/handbook-v2/Type Manipulation

1 file changed

+123
-0
lines changed

packages/documentation/copy/en/handbook-v2/Type Manipulation/Generics.md

Lines changed: 123 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -426,3 +426,126 @@ A generic parameter default follows the following rules:
426426
- If a default type is specified and inference cannot choose a candidate, the default type is inferred.
427427
- A class or interface declaration that merges with an existing class or interface declaration may introduce a default for an existing type parameter.
428428
- A class or interface declaration that merges with an existing class or interface declaration may introduce a new type parameter as long as it specifies a default.
429+
430+
## Variance Annotations
431+
432+
> This is an advanced feature for solving a very specific problem, and should only be used in situations where you've identified a reason to use it
433+
434+
[Covariance and contravariance](https://en.wikipedia.org/wiki/Covariance_and_contravariance_(computer_science)) are type theory terms that describe what the relationship between two generic types is.
435+
Here's a brief primer on the concept.
436+
437+
For example, if you have an interface representing an object that can `make` a certain type:
438+
```ts
439+
interface Producer<T> {
440+
make(): T;
441+
}
442+
```
443+
We can use a `Producer<Cat>` where a `Producer<Animal>` is expected, because a `Cat` is an `Animal`.
444+
This relationship is called *covariance*: the relationship from `Producer<T>` to `Producer<U>` is the same as the relationship from `T` to `U`.
445+
446+
Conversely, if you have an interface that can `consume` a certain type:
447+
```ts
448+
interface Consumer<T> {
449+
consume: (arg: T) => void;
450+
}
451+
```
452+
Then we can use a `Consumer<Animal>` where a `Consumer<Cat>` is expected, because any function that is capable of accepting a `Cat` must also be capable of accepting an `Animal`.
453+
This relationship is called *contravariance*: the relationship from `Consumer<T>` to `Consumer<U>` is the same as the relationship from `U` to `T`.
454+
Note the reveral of direction as compared to covariance! This is why contravariance "cancels itself out" but covariance doesn't.
455+
456+
In a structural type system like TypeScript's, covariance and contravariance are naturally emergent behaviors that follow from the definition of types.
457+
Even in the absence of generics, we would see covariant (and contravariant) relationships:
458+
```ts
459+
interface AnimalProducer {
460+
make(): Animal;
461+
}
462+
463+
// A CatProducer can be used anywhere an
464+
// Animal producer is expected
465+
interface CatProducer {
466+
make(): Cat;
467+
}
468+
```
469+
470+
TypeScript has a structural type system, so when comparing two types, e.g. to see if a `Producer<Cat>` can be used where a `Producer<Animal>` is expected, the usual algorithm would be structurally expand both of those definitions, and compare their structures.
471+
However, variance allows for an extremely useful optimization: if `Producer<T>` is covariant on `T`, then we can simply check `Cat` and `Animal` instead, as we know they'll have the same relationship as `Producer<Cat>` and `Producer<Animal>`.
472+
473+
Note that this logic can only be used when we're examining two instantiations of the same type.
474+
If we have a `Producer<T>` and a `FastProducer<U>`, there's no guarantee that `T` and `U` necessarily refer to the same positions in these types, so this check will always be performed structurally.
475+
476+
Because variance is a naturally emergent property of structural types, TypeScript automatically *infers* the variance of every generic type.
477+
**In extremely rare cases** involving certain kinds of circular types, this measurement can be inaccurate.
478+
If this happens, you can add a variance annotation to a type parameter to force a particular variance:
479+
```ts
480+
// Contravariant annotation
481+
interface Consumer<in T> {
482+
consume: (arg: T) => void;
483+
}
484+
485+
// Covariant annotation
486+
interface Producer<out T> {
487+
make(): T;
488+
}
489+
490+
// Invariant annotation
491+
interface ProducerConsumer<in out T> {
492+
consume: (arg: T) => void;
493+
make(): T;
494+
}
495+
```
496+
Only do this if you are writing the same variance that *should* occur structurally.
497+
498+
> Never write a variance annotation that doesn't match the structural variance!
499+
500+
It's critical to reinforce that variance annotations are only in effect during an instantiation-based comparison.
501+
They have no effect during a structural comparison.
502+
For example, you can't use variance annotations to "force" a type to be actually invariant:
503+
```ts
504+
// DON'T DO THIS - variance annotation
505+
// does not match structural behavior
506+
interface Producer<in out T> {
507+
make(): T;
508+
}
509+
510+
// Not a type error -- this is a structural
511+
// comparison, so variance annotations are
512+
// not in effect
513+
const p: Producer<string | number> = {
514+
make(): number {
515+
return 42;
516+
}
517+
}
518+
```
519+
Here, the object literal's `make` function returns `number`, which we might expect to cause an error because `number` isn't `string | number`.
520+
However, this isn't an instantiation-based comparison, because the object literal is an anonymous type, not a `Producer<string | number>`.
521+
522+
> Variance annotations don't change structural behavior and are only consulted in specific situations
523+
524+
It's very important to only write variance annotations if you absolutely know why you're doing it, what their limitations are, and when they aren't in effect.
525+
Whether TypeScript uses an instantiation-based comparison or structural comparison is not a specified behavior and may change from version to version for correctness or performance reasons, so you should only ever write variance annotations when they match the structural behavior of a type.
526+
Don't use variance annotations to try to "force" a particular variance; this will cause unpredictable behavior in your code.
527+
528+
> Do NOT write variance annotations unless they match the structural behavior of a type
529+
530+
Rmember, TypeScript can automatically infer variance from your generic types.
531+
It's almost never necessary to write a variance annotation, and you should only do so when you've identified a specific need.
532+
Variance annotations *do not* change the structural behavior of a type, and depending on the situation, you might see a structural comparison made when you expected an instantiation-based comparison.
533+
Variance annotations can't be used to modify how types behave in these structural contexts, and shouldn't be written unless the annotation is the same as the structural definition.
534+
Because this is difficult to get right, and TypeScript can correctly infer variance in the vast majority of cases, you should not find yourself writing variance annotations in normal code.
535+
536+
> Don't try to use variance annotations to change typechecking behavior; this is not what they are for
537+
538+
You *may* find temporary variance annotations useful in a "type debugging" situation, because variance annotations are checked.
539+
TypeScript will issue an error if the annotated variance is identifiably wrong:
540+
```ts
541+
// Error, this interface is definitely contravariant on T
542+
interface Foo<out T> {
543+
consume: (arg: T) => void;
544+
}
545+
```
546+
However, variance annotations are allowed to be stricter (e.g. `in out` is valid if the actual variance is covariant).
547+
Be sure to remove your variance annotations once you're done debugging.
548+
549+
Lastly, if you're trying to maximize your typechecking performance, *and* have run a profiler, *and* have identified a specific type that's slow, *and* have identified variance inference specifically is slow, *and* have carefully validated the variance annotation you want to write, you *may* see a small performance benefit in extraordinarily complex types by adding variance annotations.
550+
551+
> Don't try to use variance annotations to change typechecking behavior; this is not what they are for

0 commit comments

Comments
 (0)