Will you be my covariant?

Agenda

  • Refresher
  • Pop Quiz
  • Purpose
  • What is variance?
  • What makes something covariant
  • What makes something contravariant
  • What makes something Invariant
  • Another Quiz
  • Demo

Refresher

What is inheritance: example

What is inheritance: more generically

What is inheritance?

interface Animal {
  live: boolean
};

interface Cat extends Animal {
  meow: void
};

declare const cat: Cat;
cat.live // ok
cat.meow // ok

// subtype polymorphism
const animal: Animal = cat;
animal === cat // true
animal.live // ok
animal.meow // meow does not exist on animal

It's complicated

declare const cat: Cat;

const animal: Animal = cat; // ok
declare const cats: Array<Cat>;

const animals: Array<Animal> = cats; // maybe

Is this ok?

If this is ok

Pop Quiz

Can we do this?

const cats: Cat[] = [];
const dogs: Dog[] = [];
const animals: Animal[] = cats;

declare const dog: Dog;
animals.push(dog);

cats[0].meow;

Is this legal?

Yes!

But it shouldn't be

That's why variance is important

Purpose

Purpose

Understand how it is helpful

Understand when variance applies

Understand how to profit from it

What is variance?

What is variance?

Variance refers to the subtyping relationship between complex types and the subtyping relationship of their components.

What is variance?

Covariance

Covariance

interface Animal {
  live: boolean
};

interface Cat extends Animal {
  meow: () => void
};

interface List<T> {
  peek: () => T
}

declare const cats: List<Cat> ;
const animals: List<Animal> = cats; // ok

Covariance

interface Animal {
  live: boolean
};

interface Cat extends Animal {
  meow: () => void
};

interface List<T> {
  peek: () => T
}

declare const animals: List<Animal>;
const cats: List<Cat> = animals; // error
// Property 'meow' is missing in type 'Animal' 
// but required in type 'Cat'

Contravariance

Contravariance

interface Animal {
  live: boolean
};

interface Cat extends Animal {
  meow: () => void
};

type CardboardBox<T> = {
  put: (t: T) => void
}

declare const animal: CardboardBox<Animal>;
const cat: CardboardBox<Cat> = animal // actually ok;

Contravariance

interface Animal {
  live: boolean
};

interface Cat extends Animal {
  meow: () => void
};

type CardboardBox<T> = {
  put: (t: T) => void
}

declare const cat: CardboardBox<Cat>;
const animal: CardboardBox<Animal> = cat; // error

Contravariance

So why is Array covariant but CardboardBox Contravariant?

We'll get to it!

But first, invariance.

Invariant

Invariant

interface Animal {
  live: boolean
};

interface Cat extends Animal {
  meow: () => void
};

type Groomer<T> = {
  groom: (t: T) => T
}

declare const animal: Groomer<Animal>;
declare const cat: Groomer<Cat>;

const catGroomer: Groomer<Cat> = animal; // error
const animalGroomer: Groomer<Animal> = cat; // error
// Type 'Animal' is not assignable to type 'Cat'.

Bivariance

Bivariance

interface Animal {
  live: boolean
};

interface Cat extends Animal {
  meow: () => void
};

type Vet<T> = {
  treat(t: T): void
}

declare const animal: Vet<Animal>;
declare const cat: Vet<Cat>;

const catVet: Vet<Cat> = animal; // ok
const animalVet: Vet<Animal> = cat; // ok

How do we determine variance?

How do we determine variance?

In some languages you can specify

class List[+T] {
  // List is covariant on T
}

class Printer[-T] {
  // Printer is Contravariant on T
}

Typescript allows to infer it by position as well

What makes something covariant?

What makes something covariant?

interface Animal {
  live: boolean
};

interface Cat extends Animal {
  meow: () => void
};

interface List<T> {
  peek: () => T
}

What makes something covariant?

interface List<T> {
  peek: () => T
}

declare const cats: List<Cat>;
const animals: List<Animal> = cats;
const animal = animals.peek(); // ok

Is List<T> covariant?

What makes something covariant?

declare const animals: List<Animal>;
const cats: List<Cat> = animals;

Is List<T> contravariant?

const cat = cats.peek(); // error

What makes something covariant?

const animal = animals.last; // ok
interface List<T> {
  readonly last: T, // read only is important
}

declare const cats: List<Cat> ;
const animals: List<Animal> = cats;

Is List<T> covariant now? It has a field instead of a method.

What makes something covariant?

cats.last; // dangerous
// last could be a dog
declare const animals: List<Animal>;
const cats: List<Cat> = animals;

Is List<T> contravariant?

What makes something covariant?

interface List<T> {
  last: T, // writable
}

declare const cats: List<Cat> ;
const animals: List<Animal> = cats;
animals.last = new Dog() // uh oh!

Is List<T> covariant with a writable field?

Unfortunately yes

What makes something covariant?

If T is used as

Return a function

A read only field *

What makes something contravariant?

Contravariant?

interface Animal {
  live: boolean
};

interface Cat extends Animal {
  meow: () => void
};

type CardboardBox<T> = {
  put: (t: T) => void
}

Contravariant?

type CardboardBox<T> = {
  put: (t: T) => void
}

declare const animals: CardboardBox<Animal>;
const cats: CardboardBox<Cat> = animals;
cats.put(new Cat()) //ok

Is CardboardBox<T> contravariant?

Contravariant?

type CardboardBox<T> = {
  put: (t: T) => void
}
declare const cats: CardboardBox<Cat>;
const animals: CardboardBox<Animal> = cats;
animals.put(new Cat()); // safe?
animals.put(new Dog()); // No!
cats[1].meow // uh oh

Is CardboardBox<T> covariant?

What makes something contravariant?

If T is used as

an input parameter to a function

What makes something invariant?

What makes something invariant?

type Groomer<T> = {
  groom: (t: T) => T
}

If T is used in both covariant and contravariant positions

declare const animals: Groomer<Animal>;
const cats: Groomer<Cat> = animals; // error
declare const cats: Groomer<Cat>;
const animals: Groomer<Animal> = cats; // error

Another Quiz

interface Animal {
  live: boolean
};

interface Cat extends Animal {
  meow: () => void
};

interface Lilo extends Cat {
  goodGirl: true
}

interface Stitch extends Cat {
  badBoy: true
}

Another Quiz

type F0 = (c: Cat) => Cat;
type F1 = (l: Lilo) => Animal;
type F2 = (a: Animal) => Stitch;
const fn1: F0 = {} as F1 //(1)
const fn2: F0 = {} as F2 //(2)

Demo

Will you be my covariant?

By Leon Tager

Will you be my covariant?

  • 126