Variance has to do with how a generic type F<T>
varies with respect to its type parameter T
. If you know that T extends U
, then variance will tell you whether you can conclude that F<T> extends F<U>
, conclude that F<U> extends F<T>
, or neither, or both.
Covariance means that F<T>
and T
co-vary. That is, F<T>
varies with (in the same direction as) T
. In other words, if T extends U
, then F<T> extends F<U>
. Example:
Function or method types co-vary with their return types:
type Co<V> = () => V;
function covariance<U, T extends U>(t: T, u: U, coT: Co<T>, coU: Co<U>) {
u = t; // okay
t = u; // error!
coU = coT; // okay
coT = coU; // error!
}
Other (un-illustrated for now) examples are:
- objects are covariant in their property types, even though this not sound for mutable properties
- class constructors are covariant in their instance types
Contravariance means that F<T>
and T
contra-vary. That is, F<T>
varies counter to (in the opposite direction from) T
. In other words, if T extends U
, then F<U> extends F<T>
. Example:
Function types contra-vary with their parameter types (with --strictFunctionTypes
enabled):
type Contra<V> = (v: V) => void;
function contravariance<U, T extends U>(t: T, u: U, contraT: Contra<T>, contraU: Contra<U>) {
u = t; // okay
t = u; // error!
contraU = contraT; // error!
contraT = contraU; // okay
}
Other (un-illustrated for now) examples are:
- objects are contravariant in their key types
- class constructors are contravariant in their construct parameter types
Invariance means that F<T>
neither varies with nor against T
: F<T>
is neither covariant nor contravariant in T
. This is actually what happens in the most general case. Covariance and contravariance are "fragile" in that when you combine covariant and contravariant type functions, its easy to produce invariant results. Example:
Function types that return the same type as their parameter neither co-vary nor contra-vary in that type:
type In<V> = (v: V) => V;
function invariance<U, T extends U>(t: T, u: U, inT: In<T>, inU: In<U>) {
u = t; // okay
t = u; // error!
inU = inT; // error!
inT = inU; // error!
}
Bivariance means that F<T>
varies both with and against T
: F<T>
is both covariant nor contravariant in T
. In a sound type system, this essentially never happens for any non-trivial type function. You can demonstrate that only a constant type function like type F<T> = string
is truly bivariant (quick sketch: T extends unknown
is true for all T
, so F<T> extends F<unknown>
and F<unknown> extends T
, and in a sound type system if A extends B
and B extends B
, then A
is the same as B
. So if F<T>
= F<unknown>
for all T
, then F<T>
is constant).
But Typescript does not have nor does it intend to have a fully sound type system. And there is one notable case where TypeScript treats a type function as bivariant:
Method types both co-vary and contra-vary with their parameter types (this also happens with all function types with --strictFunctionTypes
disabled):
type Bi<V> = { foo(v: V): void };
function bivariance<U, T extends U>(t: T, u: U, biT: Bi<T>, biU: Bi<U>) {
u = t; // okay
t = u; // error!
biU = biT; // okay
biT = biU; // okay
}
Playground link to code
与恶龙缠斗过久,自身亦成为恶龙;凝视深渊过久,深渊将回以凝视…