Building type utils - deriving interfaces from union types in TypeScript
Tags: typescript, utils, generics
Published: 2022-05-29 23:06:48

In this post, we're going to build small, generic type utils in TypeScript.

To work with a real use case, we're going to transform a non-generic code from [my previous post](https://dev.to/maciejsmolinski/deriving-types-from-other-types-in-typescript-unions-and-interfaces-4i86) into a generic type utility, that could work with more input types, and thus be more reusable.

```ts
type Dispatcher = {
  [Message in Action as Message['type']]: Message extends { payload: any }
    ? (payload: Message['payload']) => void
    : () => void;
};
```

_<small>Non-generic code, hard to re-use ⤴</small>_ 


There are many ways to achieve this goal. We're going to pick a fancy one, **leveraging template literals, and type inference** to provide a nice development experience:


```ts
type Dispatcher =
  ValuesAsCallbacks<UnionToKeyValue<Action, 'type:payload'>>;
```

_<small>Reusable, generic type utilities ⤴</small>_

Now, let's see how to build that 🤔


## A two-step process

First, let's break down the process of turning one type into another. We'll look into the implementation and more detailed explanation in the next section.

**1️⃣  Union to Key-Value**

First, we're turning union into a key-value interface by providing a colon-delimited string literal, defining the property to be used as a key and the property to be used as a value. In case the property is missing on an item from the union, we're going to represent this fact with the `never` type:

```ts
type Action =
  | { type: 'reset' }
  | { type: 'setValue'; payload: string }
  | { type: 'setSelection'; payload: [number, number] | null }
  | { type: 'trimWhitespace'; payload: 'leading'|'trailing'|'both' };

type IntermediateResult = UnionToKeyValue<Action, 'type:payload'>;
         ▲
┌───────────────────────────────────────────────────────────────────┐
│ type IntermediateResult = {                                       │
│   reset: never;                                                   │
│   setValue: string;                                               │
│   setSelection: [number, number] | null;                          │
│   trimWhitespace: 'leading'|'trailing'|'both';                    │
│ }                                                                 │
└───────────────────────────────────────────────────────────────────┘
```

Can you already see how that is going to turn into an interface with callbacks?

**2️⃣  Values As Callbacks**

Second, we're transforming that key-value interface into key-callback interface, where the callback function in the latter is accepting a parameter of the same type as the value in the former:

```ts
type IntermediateResult = {
  reset: never;
  setValue: string;
  setSelection: [number, number] | null;
  trimWhitespace: 'leading'|'trailing'|'both';
}

type Dispatcher = ValuesAsCallbacks<IntermediateResult>;
         ▲
┌───────────────────────────────────────────────────────────────────┐
│ type Dispatcher = {                                               │
│   reset: () => void;                                              │
│   setValue: (payload: string) => void;                            │
│   setSelection: (payload: [number, number] | null) => void;       │
│   trimWhitespace: (payload: 'leading'|'trailing'|'both') => void; │
│ }                                                                 │
└───────────────────────────────────────────────────────────────────┘
```

## Implementation, and a bit of explanation

### Union to Key-Value

Let's take a look at how to implement the first utility type 🕵️

```ts
type UnionToKeyValue<
  T extends { [key: string]: any },  ①
  K extends string  ②
> = ...
```

> **(1)** The util accepts an interface, or a union of interfaces, indexed by keys being strings or string literals as a first parameter.
> 
> **(2)** It also requires a second parameter to be a string, or a string literal.

```ts
... = K extends `${infer Key}:${infer Value}`  ③
  ? Key extends keyof T  ④
    ? ...
    : never
  : never;
```

> **(3)** Key and Value never appeared as a type parameter of the util. Thanks to template literal types, we can infer both from the type provided as the second parameter, so long as it's a string or a string literal, and it contains a colon somewhere inside.
>
> Given `'type:payload'` provided as a second parameter, we're going to infer that Key is `'type'` and Value is `'payload'`. 
>
> If we don't find a colon in the second parameter, we fail to pattern-match and we return `never`.
> 
> **(4)** Next, with the inferred Key, we double-check it belongs to the key set of the first parameter, T — an interface, or a union of interfaces. If it does not belong there, we return `never`.

```ts
? { [A in T as A[Key]]: Value extends keyof A ? A[Value] : never }  ⑤
...
```

> **(5)** Now, we use mapped types to iterate through T, which implies T must be a union of types rather than a single interface to make mapping possible. With an interface, we'd see the use of `keyof` there.
>
> The use of `as` combined with indexed access `A[Key]` also gives a clue about elements of the union being interfaces and not primitive types such as undefined or null.
>
> Finally, if `A[Key]` does not exist in the element, we're not going to include it in the output. We're not that strict with the Value, if `A[Value]` does not exist, we represent that as `never`.

**Final code**

```ts
type UnionToKeyValue<
  T extends { [key: string]: any },
  K extends string
> = K extends `${infer Key}:${infer Value}`
  ? Key extends keyof T
    ? { [A in T as A[Key]]: Value extends keyof A ? A[Value] : never }
    : never
  : never;
```
That's most of the heavy work done!

### Values as Callbacks

Going from interfaces to interfaces should be slightly easier, let's take a look.

```ts
type ValuesAsCallbacks<T extends { [key: string]: any }> = ...  ①
```

> **(1)** This util accepts only one parameter, that must represent an interface with keys being strings or string literals and no constraints on the values.

```ts
... = {
  [K in keyof T]: T[K] extends never  ②
  ? () => void ③
  : (payload: T[K]) => void;  ④
};
```

> **(2)** T is an interface, we iterate through all keys of the provided type, represented as K. Now, we check if the value, T[K], has type `never`.
> 
> **(3)** If we detect the value is of the type `never`, we return a type representing a callback not accepting any parameters.
>
> **(4)** Otherwise, we return a type representing a single-parameter callback, where the parameter has the same type as the value in the original interface.

**Final code**

```ts
type ValuesAsCallbacks<T extends { [key: string]: any }> = {
  [K in keyof T]: T[K] extends never 
  ? () => void 
  : (payload: T[K]) => void;
};
```

That was it, the last missing piece in the puzzle of turning the union of interfaces into an interface with callbacks 🎉 We made it!

## Complete Code

The complete code listing from this post can be found [under this link](https://www.typescriptlang.org/play?noFallthroughCasesInSwitch=true#code/C4TwDgpgBAggxsAlgewHZQLwCgpQD5QDeUokAXFAOQBOEAzhMJVAL475EngQWUPAA1AIYAbAK4RKAbihghIEciEATCnWDVEqAOYdUYkSNbsCxUjyr8AyhBEQEKVNNnzFKigG19AWwBGEagAaKB9-agBdPQMjNlxTLnIqDURvAHUAC0Rgejk4SRk5BSVVKjsVLW1mAkoNIUQRCqqqX2RgdOYWKSwscygAEUQ6OWA4dIDMKGFxehg6AGFREV8hOABrOgAedgBVVEcAFWQAaQgQKYkN+CQ0YJruMkK3ZUoAPiwXrqwAegAqH5wflB9mJqKg6FAhFAxHs0FBkAAzKBabLUeErehI1DAZBQVanAC0ADdRBJMSi0XkAV8etwoLsDsdTucIFtcPsoBAAB7ZVDKcHEDx4kBqZI6cIUISoECsQLsI4c7kQXng9SaHTvCbyrk8vlQAAGABJCFp4eMTiAWGQjSbxsyWHr2AB+KDmhU68FChFA9i4Z0CmCYoEQ8EwDzm8LiyYk6DapW6z2IgPO0PMyIUVAQQnjWK4KDpzMBdj5rPUT6-f5QQHA0HgyFConRskBCnQZE4+twRbLNZN1Hoqk0yBR6Z0WYLQzd9Ybdmx5WcQWnEVq7SRyXSlgvCaEdhhwMJoGR-ZhyKz3UZktQZ0ACgAlJhN4TkIhlHmoFfHsUKEejuE7xgH0+yhdJ0WBAA):

![type-utils-union-to-key-value--values-as-callbacks](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/x2sylhnhu6vy7tw9i0kp.gif)
 
_<small>Reusable types from this post in TypeScript playground ⤴</small>_ 

## Final words

Now that you've explored one approach, I would encourage you to try other approaches, for example, splitting `UnionAsKeyValue<T, K>` into two separate utils and see how you find it.

Good luck!