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 followsContravariant — 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 DogThis 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 strictFunctionTypesInvariant — 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 worksIn DeepPartial:
T extends (...args: never[]) => unknownnever[]in parameter position = contravariant = any param type satisfies itunknownin return position = covariant = any return type satisfies it- Together: any function matches, regardless of its signature