Skip to content

Instantly share code, notes, and snippets.

@bozhanglab49
Last active June 25, 2021 08:37
Show Gist options
  • Save bozhanglab49/172b6eb03d9a8580a5faab49288f0dd0 to your computer and use it in GitHub Desktop.
Save bozhanglab49/172b6eb03d9a8580a5faab49288f0dd0 to your computer and use it in GitHub Desktop.
TWL-Lab49/Learning TypeScript - Co-variance and Contra-variance

In TypeScript's handbook about inference of Conditional Types, it has this:

Next, for each type variable introduced by an infer (more later) declaration within U collect a set of candidate types by inferring from T to U (using the same inference algorithm as type inference for generic functions). For a given infer type variable V, if any candidates were inferred from co-variant positions, the type inferred for V is a union of those candidates. Otherwise, if any candidates were inferred from contra-variant positions, the type inferred for V is an intersection of those candidates. Otherwise, the type inferred for V is never.

It throws out co-variant and contra-variant positions.

So, what are co-variant and contra-variant positions or in other words, what are co-variance and contra-variance?

Here is the definition but it's a bit verbose, so let me give my two cents here (please correct me):

In a nutshell, co-variance and contra-variance are about the relationship between types with the former being related in same direction while the latter in the opposite. Let's say we have type T and T' related and S and S' related in the same way; S is assignable to T (T = S); if S' is also assignable to T' (T' = S'), then the relationship between T and T' is called co-variance; if T' is assignable to S', then it is contra-variance.

In Java:

List<? extends Number> list = new ArrayList<Integer>();

Here ArrayList is assignable to List in the same direction as Integer is assignable to Number, so it's co-variance.

List<? super Integer> list = new ArrayList<Number>();

Here ArrayList is assignable to List in the opposite direction as Integer is assignable to Number, so it's contra-variance.

In TypeScript:

const numbers: number[] = [1, 2];
const numberOrStrings: Array<number | string> = numbers;

Here number[] is assignable to Array<number | string> in the same direction as number is assignable to number | string, so it's co-variance.

const double1 = (a: number | string): string => typeof a === 'number' ? a + a + '' : a + a;
const double2: (a: number) => string = double1;

Here double1 is assignable to double2 in the opposite direction as number is assignable to number | string, so it's contra-variance.

The takeaway is in TypeScript, functions are types and we are able to assign one function to another. Consequently, function and its parameters create contra-variance which is why TypeScript complains if we switch the parameter types from above example:

const double1 = (a: number): string => a + a + '';
const double2: (a: number | string) => string = double1;
// ERROR: double1 is not assignable to double2.

It should be no sweat to make out why TypeScript gives this error. The subtype function (double1) should be able to handle more broadly than the supertype (double2). Here since double2 accepts string while double1 is not able to handle string, it eliminates type safety TypeScript provides, hence the error.

Now let's go to examples TypeScript provides for the two positions in Conditional Types:

Co-variant

type Foo<T> = T extends { a: infer U; b: infer U } ? U : never;

type T1 = Foo<{ a: string; b: string }>;
//   ^ = type T1 = string
type T2 = Foo<{ a: string; b: number }>;
//   ^ = type T2 = string | number

In order to make { a: string; b: number } assignable to { a: infer U; b: infer U }, we have to make both number and string assignable to U since { a: infer U; b: infer U } is co-variant of U; hence U is inferred as string | number.

Contra-variant

type Bar<T> = T extends { a: (x: infer U) => void; b: (x: infer U) => void }
  ? U
  : never;

type T1 = Bar<{ a: (x: string) => void; b: (x: string) => void }>;
//   ^ = type T1 = string
type T2 = Bar<{ a: (x: string) => void; b: (x: number) => void }>;
//   ^ = type T2 = never

In order to make { a: (x: string) => void; b: (x: number) => void } assignable to { a: (x: infer U) => void; b: (x: infer U) => void }, we have to make U assignable to both number and string since { a: (x: infer U) => void; b: (x: infer U) => void } is contra-variant of U; hence U is inferred as string & number.

Happy Learning :)

@aztack
Copy link

aztack commented Jun 25, 2021

Brilliant!

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment