Skip to main content

Documentation Index

Fetch the complete documentation index at: https://docs.syntblaze.com/llms.txt

Use this file to discover all available pages before exploring further.

Variance annotations in TypeScript are explicit modifiers applied to generic type parameters to declare how the subtyping relationship of the generic type relates to the subtyping relationship of its type arguments. By using the in and out keywords, developers can explicitly define a type parameter as contravariant, covariant, or invariant, bypassing the compiler’s default structural variance inference.

Syntax

Variance annotations are placed directly before the type parameter in a generic declaration:
type Covariant<out T> = { /* ... */ };
type Contravariant<in T> = { /* ... */ };
type Invariant<in out T> = { /* ... */ };

Variance Mechanics

To understand the annotations, consider two types, Sub and Super, where Sub is a subtype of Super (e.g., Sub is assignable to Super).

Covariance (out)

The out modifier declares that the generic type’s subtyping direction matches the type parameter’s subtyping direction. It indicates that the type parameter is strictly produced (emitted) by the type.
  • Rule: If Sub extends Super, then Generic<Sub> is assignable to Generic<Super>.
  • Enforcement: The compiler will throw an error if an out type parameter appears in a contravariant position (e.g., as a method argument).

Contravariance (in)

The in modifier declares that the generic type’s subtyping direction is the exact reverse of the type parameter’s subtyping direction. It indicates that the type parameter is strictly consumed (received) by the type.
  • Rule: If Sub extends Super, then Generic<Super> is assignable to Generic<Sub>.
  • Enforcement: The compiler will throw an error if an in type parameter appears in a covariant position (e.g., as a return type).

Invariance (in out)

Applying both modifiers declares that the generic type requires strict equality of the type parameter. Subtyping is rejected in both directions. It indicates the type parameter is both consumed and produced.
  • Rule: Generic<Sub> and Generic<Super> are mutually unassignable. Exact type matching is required.

Structural Visualization

The following code demonstrates the mechanical assignment rules enforced by variance annotations:
type Super = { a: string };
type Sub = { a: string; b: number };

// 1. Covariance (out)
type Producer<out T> = { produce: () => T };

declare let produceSub: Producer<Sub>;
declare let produceSuper: Producer<Super>;

produceSuper = produceSub; // Valid: Producer<Sub> is a subtype of Producer<Super>
// produceSub = produceSuper; // Error


// 2. Contravariance (in)
type Consumer<in T> = { consume: (arg: T) => void };

declare let consumeSub: Consumer<Sub>;
declare let consumeSuper: Consumer<Super>;

consumeSub = consumeSuper; // Valid: Consumer<Super> is a subtype of Consumer<Sub>
// consumeSuper = consumeSub; // Error


// 3. Invariance (in out)
type Processor<in out T> = { process: (arg: T) => T };

declare let processSub: Processor<Sub>;
declare let processSuper: Processor<Super>;

// processSuper = processSub; // Error
// processSub = processSuper; // Error

Compiler Validation

When a variance annotation is applied, TypeScript statically verifies that the internal structure of the type adheres to the declared variance contract.
// ERROR: Type 'T' is declared 'out' but occurs in an 'in' position.
type InvalidProducer<out T> = {
    produce: () => T;
    consume: (arg: T) => void; 
};

// ERROR: Type 'T' is declared 'in' but occurs in an 'out' position.
type InvalidConsumer<in T> = {
    consume: (arg: T) => void;
    produce: () => T;
};
By explicitly declaring variance, the TypeScript compiler skips the deep structural comparison it normally performs to infer variance, relying instead on the declared contract. This reduces the computational cost of type checking in deeply nested or highly complex generic structures.
Master TypeScript with Deep Grasping Methodology!Learn More