Skip to content

Instantly share code, notes, and snippets.

@carlos-menezes
Created May 11, 2026 17:02
Show Gist options
  • Select an option

  • Save carlos-menezes/67272a72c91138ae33346dd78fb9fad6 to your computer and use it in GitHub Desktop.

Select an option

Save carlos-menezes/67272a72c91138ae33346dd78fb9fad6 to your computer and use it in GitHub Desktop.
Covariant, Contravariant, and Invariant in Typescript

Variance describes how a generic/composite type relates to its component types when you ask "does A extend B?"


Covariant — follows the direction of the component types.

If Cat extends Animal, then Array<Cat> extends Array<Animal>. The container "preserves" the subtype direction. Return types are covariant:

type A = () => Cat;
type B = () => Animal;
// A extends B ✅ — Cat extends Animal, return type follows

Contravariant — reverses the direction.

Parameter types are contravariant — a function that accepts a broader type is "safer" than one that requires a specific type:

type A = (x: Animal) => void;
type B = (x: Cat) => void;
// A extends B ✅ — A can handle anything B can (and more)
// B does NOT extend A — B requires a Cat, but A might receive a Dog

This is why (...args: never[]) => unknown matches any function: never is the bottom type (no value can ever be never), so contravariance means any parameter type satisfies never extends T.


Bivariant — allows both directions (unsound but practical).

TypeScript's method syntax (method(): void) is bivariant for historical reasons. Function properties (prop: () => void) are contravariant in strict mode:

interface A { fn(x: Cat): void }    // bivariant — both directions allowed
interface B { fn: (x: Cat) => void } // contravariant in strictFunctionTypes

Invariant — neither direction is allowed; types must match exactly.

TypeScript doesn't have true invariance for most things, but you can simulate it:

type Invariant<T> = { _in: (x: T) => void; _out: () => T };
// T appears in both positions, cancelling out — only exact match works

In DeepPartial:

T extends (...args: never[]) => unknown
  • never[] in parameter position = contravariant = any param type satisfies it
  • unknown in return position = covariant = any return type satisfies it
  • Together: any function matches, regardless of its signature
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment