TypeScript Variance Annotations
Variance annotations state what a generic type does with its type parameter. The word out marks a parameter the type only produces, and in marks one it only consumes.
An out producer widens safely
Source only returns its parameter, so the out annotation holds. A source of dogs can serve wherever a source of animals is expected.
interface Animal { name: string }
interface Dog extends Animal { breed: string }
interface Source<out T> {
get(): T;
}
const dogSource: Source<Dog> = { get: () => ({ name: 'Rex', breed: 'Collie' }) };
const animalSource: Source<Animal> = dogSource;
console.log(animalSource.get().name);A producer that stores its out parameter is refused
The storing member is written as a function-typed property, and that member is checked strictly. The declaration itself breaks the out promise, so the compiler refuses it.
interface Source<out T> {
get(): T;
store: (value: T) => void;
}An in consumer narrows safely
Sink only accepts its parameter, so the in annotation holds. A sink built for animals can serve wherever a sink for dogs is expected.
interface Animal { name: string }
interface Dog extends Animal { breed: string }
interface Sink<in T> {
accept(value: T): void;
}
const animalSink: Sink<Animal> = { accept: (a) => console.log('took ' + a.name) };
const dogSink: Sink<Dog> = animalSink;
dogSink.accept({ name: 'Fido', breed: 'Pug' });A sink that reads its in parameter is refused
The read member returns the parameter, and a return position produces values. That single member breaks the in promise and the declaration is refused.
interface Sink<in T> {
accept(value: T): void;
read(): T;
}An unannotated box allows neither direction
Box both reads and writes its parameter through function-typed properties, so it carries no annotation. Widening a box of dogs into a box of animals is refused.
interface Animal { name: string }
interface Dog extends Animal { breed: string }
interface Box<T> {
get: () => T;
set: (value: T) => void;
}
const dogBox: Box<Dog> = {
get: () => ({ name: 'Rex', breed: 'Collie' }),
set: (d) => console.log(d.name),
};
const animalBox: Box<Animal> = dogBox;Example
interface Animal { name: string }
interface Dog extends Animal { breed: string }
interface Source<out T> {
get(): T;
}
interface Sink<in T> {
accept(value: T): void;
}
const kennel: Source<Dog> = { get: () => ({ name: 'Rex', breed: 'Collie' }) };
const shelter: Source<Animal> = kennel;
const logbook: Sink<Animal> = { accept: (a) => console.log('logged ' + a.name) };
const dogLog: Sink<Dog> = logbook;
logbook.accept(shelter.get());
dogLog.accept({ name: 'Fido', breed: 'Pug' });Key points
- A producer hands values of the parameter type out through return positions.
- A consumer takes values of the parameter type in through parameter positions.
- The annotation is a promise, and the checker verifies the promise against every member.
- A producer of a narrower type stands where a producer of a wider type is expected.
- A consumer of a wider type stands where a consumer of a narrower type is expected.
- A type that both produces and consumes its parameter takes no annotation and allows neither substitution.
In this example, the producer widens from dogs to animals and feeds the animal sink, while the same sink also stands as a dog sink and takes a dog directly. Each value moves only in the direction its annotation makes safe.
Note
Without an annotation the checker measures variance from the members, and the annotation must match that measurement.
Try it Yourself
Edit the code, then press Run.
Running is simulated ยท TypeScript Playground
Exercise
What does the out annotation on Source<out T> promise?