Type Variance
Variance is a topic that comes up fairly often in type systems. It is used to determine how type parameters behave with respect to subtyping.
First we'll setup a couple of classes that extend one another.
1class Noun {}2class City extends Noun {}3class SanFrancisco extends City {}We saw in the section on generic types that it is possible to use variance keywords to describe when a type parameter is used in an output position, when it is used in an input position, and when it is used in either one.
Here we'll dive deeper into each one of these cases.
Flow defaults to stricter variance than TypeScript at every position where they diverge — mutable object properties, mutable arrays, generic type parameters, and class method parameters. Variance keywords mostly align (readonly on properties, in / out on type parameters); writeonly is Flow-only, and TS's <in out T> has no Flow counterpart because Flow's default is already invariance. See Flow and TypeScript variance comparison for the per-position breakdown.
Covariance
Consider for example the type
1type CovariantOf<X> = {2 readonly prop: X;3 getter(): X;4}Here, X appears strictly in output positions: it is used to read out information
from objects o of type CovariantOf<X>, either through property accesses o.prop,
or through calls to o.getter().
Notably, there is no way to input data through the reference to the object o,
given that prop is a readonly property.
When these conditions hold, we can use the out keyword to annotate X in the definition
of CovariantOf:
1type CovariantOf<out X> = {2 readonly prop: X;3 getter(): X;4}These conditions have important implications on the way that we can treat an object
of type CovariantOf<T> with respect to subtyping. As a reminder, subtyping rules
help us answer the question: "given some context that expects values of type
T, is it safe to pass in values of type S?" If this is the case, then S is a
subtype of T.
Using our CovariantOf definition, and given that City is a subtype of Noun, it is
also the case that CovariantOf<City> is a subtype of CovariantOf<Noun>. Indeed
- it is safe to read a property
propof typeCitywhen a property of typeNounis expected, and - it is safe to return values of type
Citywhen callinggetter(), when values of typeNounare expected.
Combining these two, it will always be safe to use CovariantOf<City> whenever a
CovariantOf<Noun> is expected.
A commonly used example where covariance is used is ReadonlyArray<T>.
Just like with the prop property, one cannot use a ReadonlyArray reference to write data
to an array. This allows more flexible subtyping rules: Flow only needs to prove that
S is a subtype of T to determine that ReadonlyArray<S> is also a subtype
of ReadonlyArray<T>.
The this type is restricted to covariant positions
The this type follows the same rule. It's allowed in covariant positions — method return types and readonly fields — but rejected in method parameters and mutable fields, with [incompatible-variance]:
1class Builder {2 add(x: number): this { return this; } // OK: return type is covariant3 readonly origin: this | null = null; // OK: readonly field is covariant4 takesSelf(other: this): void {} // ERROR: input positionincompatible-varianceCannot use this [1] in an input position because this [1] is expected to occur only in output positions.5 parent: this | null = null; // ERROR: invariant fieldincompatible-varianceCannot use this [1] in an input/output position because this [1] is expected to occur only in output positions.6}This falls out of the same model that makes mutable object properties invariant: if parent: this | null were allowed on a base Builder, a subclass instance could be passed as a Builder, a bare Builder written into its parent, and a later sb.parent.subclassMethod() would type-check but crash at runtime — so Flow rejects the field outright.
The rewrite when you hit this is to name the class explicitly in the input or field position (other: Builder, parent: Builder | null) and accept the loss of the subclass type at that slot — or mark the field readonly so the position becomes covariant.
Invariance
Let's see what happens if we try to relax the restrictions on the use of X and make,
for example, prop be a read-write property. We arrive at the type definition
1type NonCovariantOf<X> = {2 prop: X;3 getter(): X;4};Let's also declare a variable nonCovariantCity of type NonCovariantOf<City>.
Now, it is not safe to consider nonCovariantCity as an object of type NonCovariantOf<Noun>.
Were we allowed to do this, we could write a Noun into prop, invalidating the original type.
Flow catches this:
1class Noun {}2class City extends Noun {}3
4type NonCovariantOf<X> = {5 prop: X;6 getter(): X;7};8
9declare const nonCovariantCity: NonCovariantOf<City>;10const nonCovariantNoun: NonCovariantOf<Noun> = nonCovariantCity; // Error!incompatible-typeCannot assign nonCovariantCity to nonCovariantNoun because in type argument X [1]: Noun [2] is incompatible with City [3].What distinguishes NonCovariantOf from the CovariantOf definition is that type parameter X is used both
in input and output positions, as it is being used to both read and write to
property prop. Such a type parameter is called invariant and is the default case
of variance, thus requiring no prepending keyword:
1type InvariantOf<X> = {2 prop: X;3 getter(): X;4 setter(X): void;5};Assuming a variable invariantCity of type InvariantOf<City>,
it is not safe to use invariantCity in a context where:
- an
InvariantOf<Noun>is needed, because we should not be able to write aNounto propertyprop. - an
InvariantOf<SanFrancisco>is needed, because readingpropcould return aCitywhich may not beSanFrancisco.
In other words, InvariantOf<City> is neither a subtype of InvariantOf<Noun> nor
a subtype of InvariantOf<SanFrancisco>.
Contravariance
When a type parameter is only used in input positions, we say that it is used in
a contravariant way. This means that it only appears in positions through which
we write data to the structure. We use the in keyword to annotate such a type
parameter, paired with the writeonly keyword to mark a contravariant property:
1type ContravariantOf<in X> = {2 writeonly prop: X;3 setter(X): void;4};Common contravariant positions are write-only properties and "setter" functions.
An object of type ContravariantOf<City> can be used whenever an object of type
ContravariantOf<SanFrancisco> is expected, but not when a ContravariantOf<Noun> is.
In other words, ContravariantOf<City> is a subtype of ContravariantOf<SanFrancisco>, but not
ContravariantOf<Noun>.
This is because it is fine to write SanFrancisco into a property that can have any City written
to, but it is not safe to write just any Noun.
Function parameter contravariance
Function parameters are always in an input (contravariant) position. This means a function that accepts a more specific type cannot substitute for one that accepts a more general type. This commonly surprises people when passing callbacks with exact object types:
1type Exact = {foo: string};2type Inexact = {foo: string, ...};3
4declare function acceptsExact(item: Exact): void;5declare function takesCallback(cb: (item: Inexact) => void): void;6
7takesCallback(acceptsExact); // Error!incompatible-exactCannot call takesCallback with acceptsExact bound to cb because in the first parameter: inexact Inexact [1] is incompatible with exact Exact [2].This error occurs because takesCallback may call cb with an object that has extra properties
(since Inexact allows them). The callback acceptsExact only accepts objects with exactly {foo: string},
so passing an inexact object to it would be unsound. Even though passing an exact object directly
to a function expecting an inexact one works (an exact type is a subtype of a compatible inexact type),
the function types are flipped due to contravariance.
Input and Output Positions
Flow's error messages refer to "input positions" and "output positions" when reporting variance errors. These terms correspond directly to the variance concepts described above:
- An output position is a place where a value is read out of a type: return
types, read-only properties, getter results. A type parameter marked with
out(covariant) can only appear in output positions. - An input position is a place where a value is written into a type: function
parameters, write-only properties, setter arguments. A type parameter marked
with
in(contravariant) can only appear in input positions. - A type parameter with no keyword (invariant) can appear in both input and output positions.
When you see an error like "Cannot use T in an input position because T is
expected to occur only in output positions," it means you have a type parameter
marked as covariant (out T) but you are using it somewhere that writes a value
in, such as a function parameter. Flow reports this as [incompatible-variance]:
1type Box<out T> = {2 get(): T;3 set(val: T): void; // Error [incompatible-variance]: T is in an input position but is expected only in output positionsincompatible-varianceCannot use T [1] in an input position because T [1] is expected to occur only in output positions.4};The fix depends on your intent: if the type genuinely needs to both read and
write T, remove the out keyword to make T invariant. If the type should only
produce values of type T (never accept them), remove the setter.
See Also
- Subtypes — the underlying subtyping relationships that variance builds on
- Generics — variance keywords on generic type parameters
- Arrays —
ReadonlyArray(covariant) vsArray(invariant) - Interfaces — covariant and contravariant interface properties
- Objects — read-only and write-only object properties
- Modernizing Legacy Flow Syntax — migrating
+/-variance sigils to thereadonly/writeonlyandin/outkeywords