Variance annotations explicitly declare how generic type parameters can be used. Here's how to apply them.
Basic Concepts#
1// Covariance (out) - type flows out
2interface Producer<out T> {
3 produce(): T;
4}
5
6// Contravariance (in) - type flows in
7interface Consumer<in T> {
8 consume(value: T): void;
9}
10
11// Invariance - type flows both ways
12interface Processor<T> {
13 process(value: T): T;
14}
15
16// Bivariance - historical default for methodsCovariance with out#
1// Producer can only output T
2interface Reader<out T> {
3 read(): T;
4 peek(): T | undefined;
5}
6
7// Animal hierarchy
8class Animal {
9 name = 'animal';
10}
11
12class Dog extends Animal {
13 breed = 'unknown';
14}
15
16// Covariance allows upcasting
17const dogReader: Reader<Dog> = {
18 read: () => new Dog(),
19 peek: () => new Dog(),
20};
21
22// Dog is subtype of Animal, so Reader<Dog> is subtype of Reader<Animal>
23const animalReader: Reader<Animal> = dogReader; // OK
24
25// This would be an error with 'out':
26interface InvalidReader<out T> {
27 read(): T;
28 // write(value: T): void; // Error: T is used in 'in' position
29}Contravariance with in#
1// Consumer can only accept T
2interface Writer<in T> {
3 write(value: T): void;
4}
5
6// Contravariance allows downcasting
7const animalWriter: Writer<Animal> = {
8 write: (animal) => console.log(animal.name),
9};
10
11// Animal is supertype of Dog, so Writer<Animal> is subtype of Writer<Dog>
12const dogWriter: Writer<Dog> = animalWriter; // OK
13
14// This would be an error with 'in':
15interface InvalidWriter<in T> {
16 write(value: T): void;
17 // read(): T; // Error: T is used in 'out' position
18}Combined Annotations#
1// Different type parameters with different variance
2interface Transform<in I, out O> {
3 transform(input: I): O;
4}
5
6// Mapper with contravariant input, covariant output
7type Mapper<in T, out U> = (value: T) => U;
8
9// Event handler pattern
10interface EventHandler<in E> {
11 handle(event: E): void;
12}
13
14// Factory pattern
15interface Factory<out T> {
16 create(): T;
17 createMany(count: number): T[];
18}
19
20// Repository pattern
21interface ReadOnlyRepository<out T> {
22 find(id: string): T | undefined;
23 findAll(): T[];
24}
25
26interface WriteOnlyRepository<in T> {
27 save(entity: T): void;
28 delete(entity: T): void;
29}
30
31interface Repository<T> extends ReadOnlyRepository<T>, WriteOnlyRepository<T> {}Type Checking Improvements#
1// Without variance annotations - may have unexpected behavior
2interface OldBox<T> {
3 value: T;
4 getValue(): T;
5 setValue(v: T): void;
6}
7
8// With explicit variance - clearer intent and stricter checking
9interface ReadonlyBox<out T> {
10 readonly value: T;
11 getValue(): T;
12}
13
14interface WritableBox<in T> {
15 setValue(v: T): void;
16}
17
18interface Box<T> extends ReadonlyBox<T>, WritableBox<T> {
19 value: T;
20}Callback Patterns#
1// Callback with contravariant parameter
2interface Callback<in T> {
3 (value: T): void;
4}
5
6// Event listener
7interface Listener<in E> {
8 onEvent(event: E): void;
9}
10
11// More specific callbacks work where less specific needed
12const animalCallback: Callback<Animal> = (a) => console.log(a.name);
13const dogCallback: Callback<Dog> = animalCallback; // OK
14
15// Promise-like with covariant result
16interface Thenable<out T> {
17 then<U>(onFulfilled: (value: T) => U): Thenable<U>;
18}State Management#
1// Read-only state selector (covariant)
2interface Selector<out T> {
3 select(): T;
4 subscribe(listener: () => void): () => void;
5}
6
7// Action dispatcher (contravariant)
8interface Dispatcher<in A> {
9 dispatch(action: A): void;
10}
11
12// Store combines both
13interface Store<S, in A> {
14 getState(): S;
15 dispatch(action: A): void;
16 subscribe(listener: () => void): () => void;
17}
18
19// Type-safe selectors
20interface StateSelector<S, out T> {
21 (state: S): T;
22}
23
24function createSelector<S, out T>(
25 selector: (state: S) => T
26): StateSelector<S, T> {
27 return selector;
28}Collection Types#
1// Immutable/read-only collection (covariant)
2interface ReadonlyList<out T> {
3 get(index: number): T | undefined;
4 [Symbol.iterator](): Iterator<T>;
5 readonly length: number;
6}
7
8// Mutable collection (invariant)
9interface MutableList<T> extends ReadonlyList<T> {
10 set(index: number, value: T): void;
11 push(value: T): void;
12}
13
14// Consumer-only collection (contravariant)
15interface Sink<in T> {
16 add(value: T): void;
17 addAll(values: Iterable<T>): void;
18}
19
20// Usage
21const dogs: ReadonlyList<Dog> = [new Dog()];
22const animals: ReadonlyList<Animal> = dogs; // OK - covariantObserver Pattern#
1// Subject with contravariant observer type
2interface Subject<in T> {
3 addObserver(observer: Observer<T>): void;
4 removeObserver(observer: Observer<T>): void;
5}
6
7// Observer with contravariant event type
8interface Observer<in T> {
9 onNext(value: T): void;
10 onError?(error: Error): void;
11 onComplete?(): void;
12}
13
14// Observable with covariant emission type
15interface Observable<out T> {
16 subscribe(observer: Observer<T>): Subscription;
17}
18
19interface Subscription {
20 unsubscribe(): void;
21}Builder Pattern#
1// Builder produces T (covariant)
2interface Builder<out T> {
3 build(): T;
4}
5
6// Configurable builder accepts configuration (contravariant)
7interface ConfigurableBuilder<in C, out T> {
8 configure(config: C): this;
9 build(): T;
10}
11
12// Usage
13interface UserConfig {
14 name: string;
15 email: string;
16}
17
18interface AdminConfig extends UserConfig {
19 permissions: string[];
20}
21
22class UserBuilder implements ConfigurableBuilder<UserConfig, User> {
23 private config: Partial<UserConfig> = {};
24
25 configure(config: UserConfig) {
26 this.config = { ...this.config, ...config };
27 return this;
28 }
29
30 build(): User {
31 return new User(this.config as UserConfig);
32 }
33}
34
35// AdminConfig is subtype of UserConfig
36// So ConfigurableBuilder<UserConfig, User> accepts AdminConfig
37const builder: ConfigurableBuilder<AdminConfig, User> = new UserBuilder();Error Handling#
1// Result type with covariant success, covariant error
2type Result<out T, out E = Error> =
3 | { ok: true; value: T }
4 | { ok: false; error: E };
5
6// Mapper for success value only
7function mapResult<T, U, E>(
8 result: Result<T, E>,
9 fn: (value: T) => U
10): Result<U, E> {
11 if (result.ok) {
12 return { ok: true, value: fn(result.value) };
13 }
14 return result;
15}
16
17// Result<Dog, Error> is subtype of Result<Animal, Error>
18const dogResult: Result<Dog> = { ok: true, value: new Dog() };
19const animalResult: Result<Animal> = dogResult; // OKBest Practices#
When to Use:
✓ Library/framework code
✓ Generic interfaces
✓ Complex type hierarchies
✓ API design
Benefits:
✓ Explicit variance intent
✓ Better error messages
✓ Faster type checking
✓ Clearer documentation
Patterns:
✓ out for producers/getters
✓ in for consumers/setters
✓ No annotation for invariant
✓ Combine for transformers
Avoid:
✗ Overusing on simple types
✗ Ignoring variance errors
✗ Breaking encapsulation
✗ Adding without understanding
Conclusion#
Variance annotations (in/out) make generic type parameter usage explicit. Use out for types that only produce values (covariant), in for types that only consume values (contravariant), and no annotation for types that do both (invariant). This improves type safety and helps TypeScript provide better error messages.