Covariance & Contravariance
debt(d7/e5/b5/t7)
Closest to 'only careful code review or runtime testing' (d7). The detection_hints specify automated=no and the only tool listed is TypeScript itself. Even with --strict/strictFunctionTypes enabled, the bivariance hole in method shorthand syntax is not caught — it requires careful code review to notice the difference between method shorthand and function property syntax, and mutable covariant array misuse is silent until runtime.
Closest to 'touches multiple files / significant refactor in one component' (e5). The quick_fix mentions enabling --strict and converting method shorthand to function property syntax throughout interfaces, plus adding in/out variance annotations where needed. This is not a single-line fix — it requires auditing all generic interfaces and method signatures, potentially touching many files across a codebase.
Closest to 'persistent productivity tax' (b5). Variance applies to web and cli contexts broadly. Every generic interface and callback-accepting API is affected by this concern. Library authors and any developer designing generic types must keep variance rules in mind continuously, but it doesn't fully define the system's shape — it's a persistent tax on type-system work rather than an architectural constraint.
Closest to 'serious trap (contradicts how a similar concept works elsewhere)' (t7). The misconception field explicitly states that method shorthand syntax (m(x: T)) is bivariant — contradicting the expectation that TypeScript would enforce contravariance consistently. Developers familiar with type theory or other typed languages expect method parameters to behave contravariantly, but TypeScript's method shorthand silently allows unsound assignments. The covariant array mutation issue is an additional well-known but non-obvious trap.
Also Known As
TL;DR
Explanation
Covariance: if Dog extends Animal, then Dog[] extends Animal[] — arrays are covariant in their element type. Producer types (return positions) are covariant. Contravariance: a function that accepts Animal is assignable to one that accepts Dog — function parameters are contravariant (you can pass a more general handler). TypeScript is structurally typed so variance is checked structurally. Method shorthand syntax (method(): void) is bivariant (both directions) for historical reasons — function property syntax (method: () => void) is strictly contravariant, which is safer. TypeScript 4.7 added explicit variance annotations: in T (contravariant), out T (covariant), in out T (invariant).
Diagram
flowchart LR
subgraph Covariant
DA[Dog extends Animal]
DA --> DA2[Dog array extends Animal array]
end
subgraph Contravariant
FN[fn Animal-void]
FN --> FN2[assignable to fn Dog-void<br/>reverses direction]
end
subgraph Invariant
INV[ReadWrite T<br/>neither sub nor super]
end
Watch Out
Common Misconception
Why It Matters
Common Mistakes
- Using method shorthand instead of function property syntax — loses contravariance checking on parameters.
- Mutating a covariant array — Dog[] assignable to Animal[] seems fine until you push a Cat into it via the Animal[] reference.
- Expecting a callback accepting a subtype to be assignable to one accepting a supertype — it's actually the reverse (contravariance).
- Not using in/out variance annotations on generic interfaces where correctness matters.
Avoid When
- Variance annotations are unnecessary for simple non-generic interfaces.
When To Use
- Designing generic interfaces that are clearly producers (out T) or consumers (in T) for safer assignments.
- Library authors ensuring their generic types compose correctly with user-defined subtypes.
Code Examples
// Method shorthand — bivariant (unsound)
interface Processor {
process(input: Dog): void; // bivariant — accepts Dog or Animal
}
// Mutable covariant array — unsafe
const dogs: Dog[] = [new Dog()];
const animals: Animal[] = dogs; // OK in TS
animals.push(new Cat()); // Runtime: dogs now contains a Cat!
// Function property — strictly contravariant (safe)
interface Processor {
process: (input: Dog) => void;
}
// TypeScript 4.7 explicit variance
interface Producer<out T> { get(): T } // covariant — only produces T
interface Consumer<in T> { set(v: T): void } // contravariant — only consumes T
interface Invariant<in out T> { transform(v: T): T } // invariant