## Introduction Link to heading

In this article, I delve into the details of a semi-obscure and sufficiently complex yet quite powerful pattern for expressing correlations between different entities or within the same entity. Over time, I’ve found it recommended multiple times to solve seemingly unrelated problems that, in reality, share a common root. The featured pattern is well presented in this pull request, although it has been available for quite some time. Furthermore, the pattern has been moderately enhanced since the `4.6`

version of the language.

I have a love-hate relationship with this pattern. The love stems from its ability to express correlations that would otherwise require risky type assertions, explicit or implicit. The hate is based on the fact that one is compelled to define the types involved in a rather unusual, I dare say non-idiomatic, and especially not easy-to-read manner.

We’ll examine an instance of the problem it solves and how to exploit it in a masterful way in the specific case. Regardless of my opinion, it remains an invaluable tool to include in your toolbox, and sometimes the only one to tackle certain situations. I will then explain what I dislike and propose a solution to mitigate these difficulties.

## The problem Link to heading

```
type NumberRecord = { kind: "n", v: number, f: (v: number) => void };
type StringRecord = { kind: "s", v: string, f: (v: string) => void };
type BooleanRecord = { kind: "b", v: boolean, f: (v: boolean) => void };
type UnionRecord = NumberRecord | StringRecord | BooleanRecord;
function processRecord(record: UnionRecord) {
record.f(record.v); // error!
// Argument of type 'string | number | boolean' is not assignable to parameter of type 'never'
}
```

By design, the code above is certainly correct, but TypeScript is unable to see the correlation between `record.v`

and `record.f`

. The meaning of the error is quickly explained: TypeScript knows that `record.f`

is a function, but it cannot determine which of the three functions it is. Therefore, for safety reasons, TypeScript requires that the parameter be acceptable in every case. It must be both a `number`

and a `string`

and a `boolean`

, but there are no values that satisfy this requirement! The intersection of `number`

, `string`

, and `boolean`

is precisely the `never`

type, which has no inhabitants.

## The pattern Link to heading

As a first step, let’s slightly modify the starting point of the problem by changing the definition of the `UnionRecord`

type as follows:

```
type UnionRecord =
| { kind: "n", v: number, f: (v: number) => void }
| { kind: "s", v: string, f: (v: string) => void }
| { kind: "b", v: boolean, f: (v: boolean) => void };
```

We have just removed the intermediate types `NumberRecord`

,`StringRecord`

and `BooleanRecord`

. The `UnionRecord`

type is a discriminated union: the discriminant is the `kind`

field, which is a string literal type.

The values of the `kind`

property can be used as keys for an object type, and it is on this simple observation that the entire pattern pivots. We must indeed define a **type map** that will serve as the backbone of the entire correlation:

```
type TypeMap = {
n: number,
s: string,
b: boolean
};
```

The type map associates the aforementioned `kind`

with the corresponding type of the `v`

field, which is also the type of the parameter for the `f`

function in the same union entry. We will see later that we have some freedom in defining the type map that underpins the entire correlation; however, in this case, this precise definition is the most natural one.

Let’s now introduce the `RecordType`

type function:

```
type RecordType<K extends keyof TypeMap> = {
kind: K,
v: TypeMap[K],
f: (v: TypeMap[K]) => void
};
```

The type function `RecordType<K>`

perfectly encodes the correspondence between the `kind`

, the types, and the two related properties. It is defined in terms of `TypeMap`

, which keys serve as an upper bound for the type parameter `K`

, and is used to correlate the `v`

field with the parameter of the `f`

function: they both have the type `TypeMap[K]`

.

`RecordType`

is nothing more than the skeleton of the union `UnionRecord`

defined in the previous snippet, a union that now we can express in the following way:

```
type UnionRecord = RecordType<'n'> | RecordType<'s'> | RecordType<'b'>;
```

Let’s take another shot at defining `processRecord`

, this time utilizing `TypeMap`

and `RecordType`

:

```
function processRecord<K extends keyof TypeMap>(record: RecordType<K>) {
record.f(record.v);
}
```

It’s worth noting that the parameter type isn’t `UnionRecord`

, nor is it a type parameter with an upper bound of `UnionRecord`

. Instead, it’s a `RecordType<K>`

, where `K`

is a type parameter with an upper bound of `TypeMap`

. Within the function, the type of `record.f`

is `(v: TypeMap[K]) => void`

, while the type of `record.v`

is `TypeMap[K]`

. Everything is good and dandy.

By defining everyting in terms of the `TypeMap`

type, TypeScript is finally able to see the correlation.

### Code obfuscation Link to heading

It is then officially recommended to merge together `RecordType`

and `UnionRecord`

to avoid the possibility of creating non-distributed records (e.g., `RecordType<"n" | "b">`

) and to automate the definition of `UnionRecord`

itself based on the entries of `TypeMap`

:

```
type TypeMap = {
n: number,
s: string,
b: boolean
};
type UnionRecord<K extends keyof TypeMap = keyof TypeMap> = {
[P in K]: {
kind: P,
v: TypeMap[P],
f: (v: TypeMap[P]) => void
}
}[K];
function processRecord<K extends keyof TypeMap>(record: UnionRecord<K>) {
record.f(record.v);
}
```

In short, what used to be `RecordType`

is now directly distributed over a subset `K`

of keys in `TypeMap`

. The default value of the type parameter is not strictly necessary, but it comes in handy when the whole union is needed.

### Partial code deobfuscation Link to heading

The following snippet shows that the correlation is maintained even when indexing a non-parametric mapped type, but defined in terms of the type map with an appropriate parametric index type:

```
type TypeMap = {
n: number,
s: string,
b: boolean
};
type UnionRecordMap = {
[P in keyof TypeMap]: {
kind: P,
v: TypeMap[P],
f: (v: TypeMap[P]) => void
}
};
type UnionRecord = UnionRecordMap[keyof TypeMap];
function processRecord<K extends keyof TypeMap>(record: UnionRecordMap[K]) {
record.f(record.v);
}
```

The keys of the mapped type `UnionRecordMap`

are indeed the keys of the type map, and the indexing `UnionRecordMap[K]`

occurs with a parametric index type `K`

whose upper bound is `keyof TypeMap`

as before.

However, there is a significant difference compared to the previous cases: with this solution, TypeScript is unable to infer the actual types of `K`

during the invocation of `processRecord`

. It will always be `keyof TypeMap`

, as shown in this playground. Both of the previous solutions do not suffer from this issue.

### Extracting the callbacks Link to heading

To showcase the power of this pattern, let’s pull out the callbacks `f`

into another structure. You’ll see that we can still correlate all of this thanks to our type map:

```
type TypeMap = {
n: number,
s: string,
b: boolean
};
type ValueRecord<K extends keyof TypeMap = keyof TypeMap> = {
[P in K]: {
kind: P,
v: TypeMap[P]
}
}[K];
type FuncRecord = {
[P in keyof TypeMap]: (x: TypeMap[P]) => void
};
function processRecord<K extends keyof TypeMap>(
recv: ValueRecord<K>,
fs: FuncRecord
) {
return fs[recv.kind](recv.v);
}
```

We have both `ValueRecord`

and `FuncRecord`

defined in terms of the type map. `ValueRecord`

is based on the “verbose” version, allowing the type parameter `K`

to be precisely inferred during the invocation of `processRecord`

. On the other hand, the definition of `FuncRecord`

can be kept as simple as possible: a non-parametric mapped type whose keys are the same as the type map.

Inside `processRecord`

, the kind of `recv`

is used to index the corresponding callback within the `FuncRecord`

structure, and that function is then invoked on the value `v`

of `recv`

. TypeScript doesn’t break a sweat.

## A more complex problem Link to heading

Let’s go back to the initial definition of records, which now only contains data. Suppose we now want to invoke a specific function on each value `v`

. Each function might have its own return value, potentially different from the others. The goal is to define a matching function that, given a `UnionRecord`

, calls the corresponding callback on its `v`

property and returns the returned value with the correct type.

```
type NumberRecord = { kind: "n", v: number };
type StringRecord = { kind: "s", v: string };
type BooleanRecord = { kind: "b", v: boolean };
type UnionRecord = NumberRecord | StringRecord | BooleanRecord;
const recfs = {
n: (n: number) => n * 2,
s: (s: string) => s.trim(),
b: (b: boolean): number => (b ? 1 : 0)
}
function match(record: UnionRecord, fs: ??): ?? {
// ??
}
```

### Attempt #1: overloads Link to heading

One initial solution to the problem involves combining a `switch`

statement with the necessary overloads for the `match`

function.

In `FuncRecord`

, the type of each parameter must be aligned with the corresponding `v`

field’s type, otherwise we couldn’t invoke such callbacks. The return type will be arbitrarily determined by the caller of `match`

, therefore we set `unknown`

here. We plan, in fact, to use `FuncRecord`

as an upper bound and to infer the types of the matching callbacks (`FS`

).

The main issue with this approach is the presence of **implicit** type assertions: there’s no guarantee that the implementation adheres to the indications of the overloads’s signatures. Seeing is believing. Also, the `switch`

itself is a bit redundant, but unfortunately we can’t just go with `fs[record.kind](record.v)`

, not without resorting to the pattern at least.

```
type NumberRecord = { kind: "n", v: number };
type StringRecord = { kind: "s", v: string };
type BooleanRecord = { kind: "b", v: boolean };
type UnionRecord = NumberRecord | StringRecord | BooleanRecord;
type FuncRecord = {
[R in UnionRecord as R["kind"]]: (v: (UnionRecord & { kind: R["kind"] })["v"]) => unknown;
}
function match<FS extends FuncRecord>(record: NumberRecord, fs: FS): ReturnType<FS["n"]>
function match<FS extends FuncRecord>(record: StringRecord, fs: FS): ReturnType<FS["s"]>
function match<FS extends FuncRecord>(record: BooleanRecord, fs: FS): ReturnType<FS["b"]>
function match<FS extends FuncRecord>(record: UnionRecord, fs: FS) {
switch(record.kind) {
case 'n': return fs["n"](record.v)
case 's': return fs["s"](record.v)
case 'b': return fs["b"](record.v)
}
}
```

### Attempt #2: messing with the return type Link to heading

Here again we are dealing with type assertions, this time explicit, with all the risks that come with them. TypeScript doesn’t yet support control flow analysis to refine a parametric type: the type of the `record`

is refined within the cases of the `switch`

, but the same doesn’t happen to the type parameter `R`

. But even if that were the case, accessing with a concrete index in `fs`

breaks all ties with the type parameters. For example, `fs["n"]`

is inferred as `(v: number) => unknown`

, that is its constraint.

```
type NumberRecord = { kind: "n", v: number };
type StringRecord = { kind: "s", v: string };
type BooleanRecord = { kind: "b", v: boolean };
type UnionRecord = NumberRecord | StringRecord | BooleanRecord;
type FuncRecord = {
[R in UnionRecord as R["kind"]]: (v: (UnionRecord & { kind: R["kind"] })["v"]) => unknown;
}
function match<R extends UnionRecord, FS extends FuncRecord>(record: R, fs: FS): ReturnType<FS[R["kind"]]> {
switch (record.kind) {
case "n":
return fs["n"](record.v) as ReturnType<FS[R["kind"]]>;
case "s":
return fs["s"](record.v) as ReturnType<FS[R["kind"]]>;
case "b":
return fs["b"](record.v) as ReturnType<FS[R["kind"]]>;
}
}
```

### Final attempt: the pattern Link to heading

It’s necessary to resort once again to the pattern discussed in this article. The solution is a generalization of the extracting callbacks case, where now each callback has its own return type. Let’s first analyze an initial attempt, which, however, will turn out to be wrong:

```
type TypeMap = {
n: number,
s: string,
b: boolean
};
type ValueRecord<K extends keyof TypeMap = keyof TypeMap> = {
[P in K]: {
kind: P;
v: TypeMap[P];
};
}[K];
type FuncRecord = {
[P in keyof TypeMap]: (v: TypeMap[P]) => unknown;
};
function match<K extends keyof TypeMap, FS extends FuncRecord>(
recv: ValueRecord<K>,
fs: FS
): ReturnType<FS[K]> {
return fs[recv.kind](recv.v);
}
```

`ValueRecord`

is defined in the same wordy way as before, whereas `FuncRecord`

is nothing more than a mapped type based on the keys of the type map. Inside the match function, the kind of `recv`

is again used to index the corresponding function within `fs`

, and this function is then invoked on the `v`

value of `recv`

. The type returned by `match`

is expressed in terms of `FS`

, as it was in the previuous attempts.

Unfortunately, however, we encounter an error: the type of `fs[recv.kind]`

has been inferred as `never`

, and `string | number | boolean`

is not assignable to parameter of type `never`

. What a drag. What’s the problem now?

### Final final attempt Link to heading

The problem is in the type of `fs`

, that is the type parameter `FS`

. We declared that type parameter to infer the return type of the matching callbacks, so that the `match`

function can be more flexible. It is true that the upper bound of `FS`

, namely `FuncRecord`

, specifies the input types as a direct dependency of the type map, but it seems like this connection gets lost with `FS`

, a generic subtype of it. This loss could be related to variance, as variance rules allow passing callbacks whose input type is a supertype compared to what is declared by `FuncRecord`

. Also, it happens again that accessing with a specific index in `fs`

breaks all ties with the type parameters: the inferred return type is once again `unknown`

.

The solution I propose to overcome these issues is based on a recently merger PR and will work from version `5.4`

of the language. However, I’m open to suggestions and alternative ideas.

```
type TypeMap = {
n: number,
s: string,
b: boolean
};
type ValueRecord<K extends keyof TypeMap = keyof TypeMap> = {
[P in K]: {
kind: P;
v: TypeMap[P];
};
}[K];
type FuncRecord = {
[P in keyof TypeMap]: (v: TypeMap[P]) => unknown;
};
function match<K extends keyof TypeMap, FS extends FuncRecord>(
recv: ValueRecord<K>,
fs: { [K in keyof FS & keyof FuncRecord]: FS[K] & ((v: TypeMap[K]) => ReturnType<FS[K]>) }
): ReturnType<FS[K]> {
return fs[recv.kind](recv.v);
}
```

We need both the ability to infer the current type of the matching callbacks and to maintain the relationship with the type map in the inferred types. We use a reverse mapped type to get both things done. The upper bound of `FS`

ensures that `fs`

will have **all** the keys specified in `FuncRecord`

, while the intersection in the reverse mapped type with `keyof FuncRecord`

ensures that `fs`

will have **only** the keys specified in `FuncRecord`

.

The key we specify in the reverse mapped type is kinda special: we need that `FS[K]`

to make sure there’s a candidate for each key `K`

from which the reverse mapped type can figure out the value of `FS[K]`

. Then, by intersecting it with `(v: TypeMap[K]) => ReturnType<FS[K]>`

, we ensure the connection with the type map for the input type, without really imposing anything on the return type of the matching callbacks. Moreover, we need to make sure TypeScript knows that for each key `K`

, the corresponding callback in `fs`

actually returns `ReturnType<FS[K]>`

, since that is the return type of the `match`

function.

## What I don’t like Link to heading

It is usually more idiomatic to first define the components of a (discriminated) union and then create the union using the `|`

operator, rather than encapsulating everything in a complex and obscure type function, as done with `ValueRecord`

:

```
// before:
type NumberRecord = { kind: "n"; v: number };
type StringRecord = { kind: "s"; v: string };
type BooleanRecord = { kind: "b"; v: boolean };
type UnionRecord = NumberRecord | StringRecord | BooleanRecord;
// now:
type TypeMap = {
n: number,
s: string,
b: boolean
};
type ValueRecord<K extends keyof TypeMap = keyof TypeMap> = {
[P in K]: {
kind: P;
v: TypeMap[P];
};
}[K];
```

It’s worth noting that it’s not always feasible to straightforwardly apply this latter approach, especially when the union’s components have different structures. We are forced to abuse the type map, and things quickly go south.

### An alternative Link to heading

My efforts have focused on keeping the definitions of `UnionRecord`

components separate, as is idiomatic. The rest remains almost unchanged.

So, let’s start by defining the types of the records:

```
type NumberRecord = { kind: "n", v: number };
type StringRecord = { kind: "s", v: string };
type BooleanRecord = { kind: "b", v: boolean };
type UnionRecord = NumberRecord | StringRecord | BooleanRecord;
```

The type map iterates over `UnionRecord`

and associates each kind with the corresponding record:

```
// { n: NumberRecord; s: StringRecord; b: BooleanRecord; }
type TypeMap = {
[K in UnionRecord["kind"]]: Extract<UnionRecord, { kind: K }>;
};
```

The definition of `ValueRecord`

gets a bit complicated. We are forced to manually define the types of the `kind`

and `v`

fields, always based on the type map, because these two fields are the ones directly accessed by the match function. The type `{ kind: P, v: TypeMap[P]["v"] } & Omit<TypeMap[P], "kind" | "v">`

is conceptually identical to `TypeMap[P]`

, but TypeScript gets lost within `match`

if we use the latter. Note that in this specific example, the intersection with `Omit<TypeMap[P], "kind" | "v">`

could be omitted since records don’t have any other properties besides `kind`

and `v`

.

```
type ValueRecord<K extends keyof TypeMap = keyof TypeMap> = {
[P in K]: { kind: P, v: TypeMap[P]["v"] } & Omit<TypeMap[P], "kind" | "v">
}[K];
```

After that, we just need to align the parameter type both in `FuncRecord`

and in the `match`

function:

```
type FuncRecord = {
[P in keyof TypeMap]: (x: TypeMap[P]["v"]) => unknown;
};
function match<K extends keyof TypeMap, FS extends FuncRecord>(
recv: ValueRecord<K>,
fs: { [K in keyof FS & keyof FuncRecord]: FS[K] & ((v: TypeMap[K]["v"]) => ReturnType<FS[K]>) }
): ReturnType<FS[K]> {
return fs[recv.kind](recv.v);
}
```

Here you can find playground with the whole code.

### An alternative to the alternative Link to heading

To be fair, experimenting with TypeScript and type parameters, I was able to get a somewhat less safe solution that doesn’t employ the pattern:

```
type NumberRecord = { kind: "n", v: number };
type StringRecord = { kind: "s", v: string };
type BooleanRecord = { kind: "b", v: boolean };
type UnionRecord = NumberRecord | StringRecord | BooleanRecord;
type Kinds = UnionRecord["kind"];
type GetValue<K extends Kinds> = (UnionRecord & { kind: K })["v"];
type RecFs<K, V, R> = {
[KK in Kinds]: KK extends K ? (v: V) => R : (...args: any) => any
};
function match<
K extends Kinds,
V extends GetValue<K>
>(record: { kind: K; v: V }) {
return <R>(fs: RecFs<K, V, R>): R => fs[record.kind](record.v);
}
```

Here, we have expanded the structure of a `record`

in the type `{ kind: K; v: V }`

, where `K`

is the type parameter used to infer the `kind`

, and as such, it must be assignable to `"n" | "s" | "b"`

. Meanwhile, `V`

is the type parameter used to infer the type of the value `v`

corresponding to the kind `K`

. The type of the structure containing the functions enforces the presence of a function for each `kind`

. However, it enforces the correct type of the `v`

argument and infers the return type `R`

only in the callback corresponding to the `kind`

`K`

.

I describe this solution as less safe because, despite the upper bounds on the type parameters, we lack certainty that the `record`

passed to `match`

is indeed a `UnionRecord`

:

```
// allowed
match({
kind: Math.random() > 0.5 ? "s" : "n",
v: Math.random() > 0.5 ? "ciao" : 123
});
```

### A safer alternative to the alternative of the alternative Link to heading

At this point I’m just trolling, and no, I have no intention of explaining why this stuff works. I’m afraid I might not even know. Or maybe I do. Who knows.

```
type NumberRecord = { kind: "n", v: number };
type StringRecord = { kind: "s", v: string };
type BooleanRecord = { kind: "b", v: boolean };
type UnionRecord = NumberRecord | StringRecord | BooleanRecord;
type Kinds = UnionRecord["kind"];
type GetValue<K extends Kinds> = (UnionRecord & { kind: K })["v"];
type RecFs<K, V, R> = {
[KK in Kinds]: KK extends K ? (v: V) => R : (...args: any) => any
};
function match<
U extends Extract<UnionRecord, { kind: K, v: V }>,
K extends Kinds = U["kind"],
V extends GetValue<K> = GetValue<K>,
>(record: U) {
return <R>(fs: RecFs<K, V, R>): R => fs[record.kind](record.v);
}
// not allowed anymore
match({
kind: Math.random() > 0.5 ? "s" : "n",
v: Math.random() > 0.5 ? "ciao" : 123
})
```

As always, a playground to play with.

## Conclusion Link to heading

Expressing correlations has never been this challenging. No need to thank me, but if you really insist feel free to insult me on Xitter.