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 :)
Brilliant!