Understanding Covariance, Contravariance, and Bivariance in TypeScript

This article explains TypeScript's covariant, contravariant, bivariant, and invariant type relationships, why methods and function properties behave differently under strictFunctionTypes, how ESLint's @typescript-eslint/method-signature-style rule enforces function property signatures, and practical ways to achieve safer type definitions.

ELab Team
ELab Team
ELab Team
Understanding Covariance, Contravariance, and Bivariance in TypeScript

Origin

The monorepo project encountered the ESLint rule @typescript-eslint/method-signature-style while applying a rule set.

Rule Example

Not Recommended

interface T1 {
  func(arg: string): number;
}

type T2 = {
  func(arg: boolean): void;
};

interface T3 {
  func(arg: number): void;
  func(arg: string): void;
  func(arg: boolean): void;
}

Recommended

interface T1 {
  func: (arg: string) => number;
}

type T2 = {
  func: (arg: boolean) => void;
};

// this is equivalent to the overload
interface T3 {
  func: ((arg: number) => void) &
        ((arg: string) => void) &
        ((arg: boolean) => void);
}

Reason

The rule claims that enabling it provides better function type checking when used with TypeScript's strictFunctionTypes mode.

A method and a function property of the same type behave differently. Methods are always bivariant in their argument, while function properties are contravariant in their argument under strictFunctionTypes .

Many developers are confused by the terms contravariant and why methods and function properties are treated differently.

Variance

Assignability

declare const a: string;
const b: string = a;

In TypeScript, assignability is based on structural typing (duck typing).

What are Covariance and Contravariance?

Assume Dog is a subtype of Animal. Dog can be assigned to Animal. How does this affect a List of each type?

Covariant preserves the subtype relationship (e.g., List<Dog> can be assigned to List<Animal>).

Covariant (↗) keeps the subtype ordering Subtype ≤ BaseType.

Contravariant (↘) reverses the ordering, allowing List<Animal> to be assigned to List<Dog>.

Bivariant allows both directions.

Invariant disallows any assignment between the two.

How Position Affects Variance

Return type → covariant

F1: () => Cat;
F2: () => Animal;

Parameter type → usually contravariant

const f1 = (cat: Cat) => { cat.miao(); };
const f2 = (animal: Animal) => { console.log(animal.weight); };
// error: not all Animals have miao()
const f3: (animal: Animal) => void = f1; // error
const f4: (cat: Cat) => void = f2; // ok

Function Properties vs Methods

Type Checking

interface Animal { name: string; }
interface Dog extends Animal { wang: () => void; }
interface Cat extends Animal { miao: () => void; }
interface Comparer<T> { compare(a: T, b: T): number; }
declare let animalComparer: Comparer<Animal>;
declare let dogComparer: Comparer<Dog>;
animalComparer = dogComparer; // Ok because of bivariance
dogComparer = animalComparer; // Ok
interface Animal { name: string; }
interface Dog extends Animal { wang: () => void; }
interface Cat extends Animal { miao: () => void; }
interface Comparer<T> { compare: (a: T, b: T) => number; }
declare let animalComparer: Comparer<Animal>;
declare let dogComparer: Comparer<Dog>;
animalComparer = dogComparer; // Error
dogComparer = animalComparer; // Ok

With strictFunctionTypes enabled, TypeScript treats method parameters as bivariant and function property parameters as contravariant.

JS Output Difference

Compiled JavaScript shows that methods are placed on the prototype, while function properties become instance fields.

class Greeter {
  constructor() {
    this.greet();
    this.greet2();
    this.greet3();
  }
  greet() { console.log('greet1', this); }
  greet2 = () => { console.log('greet2', this); }
  greet3 = function() { console.log('greet3', this); }
}
let bla = new Greeter();

Array Variance

TypeScript's Array<T> defines methods that are both covariant (e.g., pop) and contravariant (e.g., push), making the whole type bivariant and unsafe for mutable operations.

const dogs: Array<Dog> = [];
const animals: Animal[] = dogs; // bivariant
animals.push(new Cat()); // unsafe

Separating covariant and contravariant parts yields safer types:

// Covariant array type
interface CoArray<T> {
  pop: () => T | undefined;
  join?: (separator?: string) => string;
  // ... other read‑only methods
}
// Contravariant array type
interface ContraArray<T> {
  push: (...items: T[]) => number;
  indexOf: (searchElement: T, fromIndex?: number) => number;
}

TypeScript ultimately keeps Array bivariant for compatibility.

Summary

Prefer readonly to keep types immutable.

Use function properties instead of methods when strict type checking is needed.

Separate covariant and contravariant members or make types immutable.

Avoid bivariant types where possible.

Exercise

interface StatefulComponent {
  setState(state: "started" | "stopped"): void;
}
function moveToPausedState(component: { setState(state: "started" | "stopped" | "paused"): void }) {
  component.setState("paused");
}
const ListComponent: StatefulComponent = {
  setState(state) {
    switch (state) {
      case "started":
      case "stopped":
        console.log(state);
        break;
      default:
        console.error(state);
        break;
    }
  }
};
moveToPausedState(ListComponent);

References

@typescript-eslint/method-signature-style: https://github.com/typescript-eslint/typescript-eslint/blob/main/packages/eslint-plugin/docs/rules/method-signature-style.md

Subtype ordering: https://zh.wikipedia.org/wiki/子类型序关系

PR on strictFunctionTypes: https://github.com/microsoft/TypeScript/pull/18654

Original Source

Signed-in readers can open the original source through BestHub's protected redirect.

Sign in to view source
Republication Notice

This article has been distilled and summarized from source material, then republished for learning and reference. If you believe it infringes your rights, please contactadmin@besthub.devand we will review it promptly.

TypeScriptcovariancevariancecontravariancebivariancestrictFunctionTypes
ELab Team
Written by

ELab Team

Sharing fresh technical insights

0 followers
Reader feedback

How this landed with the community

Sign in to like

Rate this article

Was this worth your time?

Sign in to rate
Discussion

0 Comments

Thoughtful readers leave field notes, pushback, and hard-won operational detail here.