Hackle's blog

between the abstractions we want and the abstractions we get.

Check out my workshop at **NDC** { Oslo }

May 22-23 Simple by Design: Declutter Your Architecture, Code and Test

May 22-23 Simple by Design: Declutter Your Architecture, Code and Test

Why is `{ 'f1': string, 'f2': number } & { 'f2': number, 'f3': string }`

not `{ 'f2': number }`

?

And ready for some fireworks with the wild mix of union, intersection and variance?

(It must be the street-light effect that I keep going back to co-variants and contra-variants across a few languages. Most recently it's for the magic of conversion from union to tuple type.)

Give two types `A`

and `B`

, when put in a union `A | B`

, a looser type is created. If we count its values, the union type have values from `A`

+ values from `B`

, so union types are also called sum types. This feature itself is straightforward - albeit sorely missing from many main stream languages.

When an intersection is created from `A & B`

, a stricter / more specific type is created. It only has values that are both `A`

and `B`

. This can be confusing to many, especially when it comes to object types.

Quite reminiscent of algebra, we can start by looking at the intersection of unions.

```
type T1 = 'a' | 'b';
type T2 = 'b' | 'c';
// type T3 = "b"
type T3 = T1 & T2;
```

Very intuitive and true to the name "intersection". But how about this?

```
type T4 = { 'f1': string, 'f2': number };
type T5 = { 'f2': number, 'f3': string };
type T6 = T4 & T5;
```

Surely `T6`

should be `{ 'f2': number }`

?! This was exactly what I first thought. However `T6`

is actually `{ 'f1': string, 'f2': number, 'f3': string }`

. Why is that?

The reason is the `&`

operator does not really inspect the fields of the object types, instead, the intersection applies to the values of `T4`

and `T5`

. What does that mean?

Think of `T4`

and `T5`

not as objects, because they are not - they are types! If it makes any sense, turn them into interfaces.

```
interface I4 { 'f1': string, 'f2': number };
interface I5 { 'f2': number, 'f3': string };
interface I6 extends I4, I5 {}
```

A trained Object-oriented mind (that's you, yes) should grok this right away. Any value of interface `I6`

must have all 3 fields to satisfy both `I4`

and `I5`

- it's a stricter type than each.

The name "intersection" makes sense if we count the values of each type: values of `I6`

is the intersection of values of `I4`

and `I5`

.

The philosophical and curious will find there is an interesting case to the union and intersection duality. When standing alone, a type is both a single-case union and a single-case intersection on its own. This may sound like a smart-ass revelation, but it can offer some interesting perspectives.

```
type T7 = { 'f1': string };
type T8 = { 'f2': number };
type T9 = T7 | T8;
const v9_1: T9 = { 'f1': 'a' };
const v9_2: T9 = { 'f2': 1 };
type T10 = T7 & T8;
const v10: T10 = { 'f1': 'a', 'f2': 1 };
```

By using `|`

or `&`

as combinator, we can build up more complex types. For example, this naive yet fun `Parse`

type.

```
type Parse<T extends string> =
T extends `${infer T1} or ${infer T2}`
? Parse<T1> | Parse<T2>
: T extends `${infer T1} and ${infer T2}`
? Parse<T1> & Parse<T2>
: { [k in T]: true };
const p1: Parse<'a and b and c'> = { 'a': true, 'b': true, 'c': true };
const p2_a: Parse<'a or b'> = { 'a': true };
const p2_b: Parse<'a or b'> = { 'b': true };
```

Intersection and union can appear quite closely in unexpected places. An easy example is when we try to access the properties of a union type - the available keys are the intersection of the keys of all the cases of the union type.

```
// type T4 = { 'f1': string, 'f2': number };
// type T5 = { 'f2': number, 'f3': string };
type T11 = T4 | T5;
// const v11: "f2"
declare const v11: keyof T11;
// const v12: "f2", same!
declare const v12: keyof T4 & keyof T5;
```

This makes sense: only the common keys can be safe for operations such as `T11['f2']`

; but not `T11['f1']`

as it's unsafe for `T5`

.

We already know that TypeScript respects variance (previously discussed in C#). Never missing the opportunity to look at it again.

With covariance, typically when `T`

is the return type of a function.

```
type Covariant<T> = () => T;
let co1: Covariant<'a' | 'b'>;
declare const co2: Covariant<'a'>;
co1 = co2; // this is ok
declare const co3: Covariant<'a' | 'b' | 'c'>;
/*
Type 'Covariant<"a" | "b" | "c">' is not assignable to type 'Covariant<"a" | "b">'.
Type '"a" | "b" | "c"' is not assignable to type '"a" | "b"'.
Type '"c"' is not assignable to type '"a" | "b"'.ts(2322)
*/
co1 = co3;
```

However, when `T`

is a parameter type, then the assignability is reversed - quite the mind-bender!

```
type ContraVariant<T> = (arg: T) => void;
let contr1: ContraVariant<'a' | 'b'>;
declare const contr2: ContraVariant<'a' | 'b' | 'c'>;
contr1 = contr2; // this is ok!
declare const contr3: ContraVariant<'a'>;
/*
Type 'ContraVariant<"a">' is not assignable to type 'ContraVariant<"a" | "b">'.
Type '"a" | "b"' is not assignable to type '"a"'.
Type '"b"' is not assignable to type '"a"'.ts(2322)
*/
contr1 = contr3;
```

We can also give them signs: co-variance is positive and contra-variance negative. Take some time, let that sink in.

Intersection or union alone is not fun enough; things become **really** interesting when variance is involved.

Before we jump in, we need a couple of utility types from a previous post, `Contra<T>`

that takes `T`

to a contra-variant position, namely, as a parameter. Note `T`

can be any type, even a `Contra<T>`

itself. And `Co<T>`

for the opposite.

```
type Co<T> = () => T;
type Contra<T> = (arg: T) => void;
```

Then we have `InferContra`

and `InferCo`

that recovers `T`

, with an important note: if `Fn`

is a union type, it will be matched only once, as `[Fn]`

stops union distribution. So it's not the complete reverse engineering of `Co`

or `Contra`

. That is possible without the `[Fn]`

trick.

```
type InferCo<Fn> =
[Fn] extends [Co<infer T>] ? T : never;
type InferContra<Fn> =
[Fn] extends [Contra<infer T>] ? T : never;
```

Now let's see what happens.

```
/*
const infer_co1: {
f1: 'f1';
} | {
f2: 'f2';
}
*/
declare const infer_co1: InferCo<
Co<{ f1: 'f1' }> |
Co<{ f2: 'f2' }>
>;
/*
const contra1: {
f1: 'f1';
} & {
f2: 'f2';
}
*/
declare const contra1: InferContra<
Contra<{ f1: 'f1' }> |
Contra<{ f2: 'f2' }>
>;
// remember ^ is equivalent to
declare const infer_contra2: [
((arg: { f1: 'f1' }) => void) |
((arg: { f2: 'f2' }) => void)
] extends [ ((arg: infer I) => void) ]
? I
: never;
```

Do you see what's happening? `InferCo`

is plain predictable, but `InferContra`

from a union of two contra-variant types returns an intersection type! (Ok that's quite a mouthful.) Contra-variance strikes again in stunning fashion.

Can we make sense of it? Well... kind of. We can "desugar" `InferContra`

further for this specific case,

```
type InferUnionContra<Fn> =
[Fn] extends [((arg: infer T) => void) | ((arg: infer T) => void)]
? T
: never;
/*
const infer_union_contra: {
f1: 'f1';
} & {
f2: 'f2';
}
*/
declare const infer_union_contra: InferUnionContra<
Contra<{ f1: 'f1' }> |
Contra<{ f2: 'f2' }>
>;
```

Notice how `infer T`

can be used **twice** in the same `extends`

clause? That forces TypeScript to return a single type that accounts for both of its appearances - depending on its positioning (therefore variance).

But how do we know for sure that `InferContra`

is giving us the correct result?

Let's call on a pretty reputable judge of character, **Equational Reasoning** (multiple rounds of thunder and lightning)! This is done by expanding the conditional type by putting in the actual input and output.

```
/*
remember:
type Contra<T> = (arg: T) => void;
type InferContra<Fn> = [Fn] extends [Contra<infer T>] ? T : never;
*/
// the expanded form of: Contra<{ f1: 'f1' }> | Contra<{ f2: 'f2' }>
type UnionInput = ((arg: { f1: 'f1' }) => void) | ((arg: { f2: 'f2' }) => void);
type IntersectionSatisfiesEquation =
[UnionInput] extends [((arg: { f1: 'f1' } & { f2: 'f2' }) => void)] ? true : false;
```

Let's also recap: `T extend U`

holds if `T`

is a subtype of `U`

, such as `'a' extends string`

. In another word, A value of `T`

can be assigned to a variable of `U`

.

By this reasoning `InferContra`

is proof that the union `((arg: { f1: 'f1' }) => void) | ((arg: { f2: 'f2' }) => void)`

is a subtype of `((arg: { f1: 'f1' } & { f2: 'f2' }) => void)`

. This can be proven as below.

```
declare let contra_union: ((arg: { f1: 'f1' }) => void) | ((arg: { f2: 'f2' }) => void);
declare let contra_intersection: ((arg: { f1: 'f1' } & { f2: 'f2' }) => void);
// a subtype can be assigned to a supertype
contra_intersection = contra_union;
```

Which, by distribution, means each case of the union type `((arg: { f1: 'f1' }) => void) | ((arg: { f2: 'f2' }) => void)`

can be assigned to the intersection `((arg: { f1: 'f1' } & { f2: 'f2' }) => void)`

. By variance manipulation, this further means `{ f1: 'f1' } & { f2: 'f2' }`

is a subtype (mind the shift of direction) of either `{ f1: 'f1' }`

or `{ f2: 'f2' }`

, which is true, as we have discussed in the beginning. Check!

Or, `{ f1: 'f1' }`

is a super type of `{ f1: 'f1' } & { f2: 'f2' }`

, and `((arg: { f1: 'f1' } & { f2: 'f2' }) => void)`

is a super type of `(arg: { f1: 'f1' }) => void)`

.

We are not done yet. Knowing how contra-variance can be thought of as the minus sign, and double minus equals plus (genius!), we can test out the thoroughness of this behaviour (or feature). It's showtime.

```
type InferContra2<T> =
[T] extends [Contra<Contra<infer I>>] ? I : never;
type InferContra3<T> =
[T] extends [Contra<Contra<Contra<infer I>>>] ? I : never;
type InferContra4<T> =
[T] extends [Contra<Contra<Contra<Contra<infer I>>>>] ? I : never;
/*
const contra2: {
f1: 'f1';
} | {
f2: 'f2';
}
*/
declare const contra2: InferContra2<
Contra<Contra<{ f1: 'f1' }>> |
Contra<Contra<{ f2: 'f2' }>>
>;
/*
const contra3: {
f1: 'f1';
} & {
f2: 'f2';
}
*/
declare const contra3: InferContra3<
Contra<Contra<Contra<{ f1: 'f1' }>>> |
Contra<Contra<Contra<{ f2: 'f2' }>>>
>;
/*
const contra4: {
f1: 'f1';
} | {
f2: 'f2';
}
*/
declare const contra4: InferContra4<
Contra<Contra<Contra<Contra<{ f1: 'f1' }>>>> |
Contra<Contra<Contra<Contra<{ f2: 'f2' }>>>>
>;
```

I'll be damned... It is CHECK, CHECK and CHECK. The result types alternate between union and intersection. TypeScript really thought this through. I am impressed.

Link to the source code used above.