An unnecessary complexity tale on Typescript-land.
Tags: typescript, webdev, tutorial
Published: 2022-04-02 05:39:13

Twitter is both a blessing and a curse as anyone who's fond of the platform can attest to. Today, I've found a small challenge lurking within a tweet under a hashtag that I don't really follow or have any interest whatsoever but sometimes it pops up (???)...

The challenger was really straight forward:

![oleg008 Tweet](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/0vyeakzve8duao9kazht.png)

As someone who likes to keep the codebase with the small amount of `any`'s possible, I clicked the [playground link](https://www.typescriptlang.org/play?ssl=22&ssc=1&pln=23&pc=1#code/C4TwDgpgBAwg9gO2AQwJYIgJwDwDVkA2ArhAHxQC8UA3gFBQNQBuhJAXFPsRLQL620AxogDOwKIMwRkwCPCRoMmSlDytoEAB6yEAExFQiCANYI4AdwSkAFC24cuJAJQd5KdFjXdyFcnUZQUsBEmAg0diT8-EKi4gBmKtbCCh6YIhwAgpiYyCDYboqeyAggpKQANBKEBABGyILGHNYAdK0REOlQxSBOlORMcKi6vb409IzJYszqBlQA2gC64wxxcMpJsRKI7kpQcAnJO1givf4BjO0izWBEIgAWGylKze1Oy1D8AYLVdQ0tbTM3tEYggpodCpgAIwqSTSWQFVLWADkd1QSLek3E4NSACYYVIZHJthDrDi3gIAPQUqAACQsUF0cCgAElAhAwAR6tBgHc4CJoN0oKBIAZ0F0oOZckK7jIJagCFAAOaoJjQAC2Gk09XEwo60240OKun1JBxlXMqB5cCI4hlTHQiqFTJEkEEqDiIGlEDVAH5aHFrHNsUpIZVg1gcQtKrZ1KGTRAyX0xnxyUA) and gave it a try.

## And... are those "Generics" with us in the room right now?

My first idea was pretty simple:
```Typescript
const f = <T extends Container<unknown>>(
  containers: T[],
  callback: (...values: any) => void
) => { ... }
```

Starting with a Generic `T` which needs to extends the `Container` type to guarantee that the `containers` parameter type is undoubtedly a `List of Containers`.

Now, how do we implement the type logic for `...values` within the `callback` parameter? After some time of weird thinking and listening to some City Pop, my _"amazing"_ head came up with this:

```Typescript
type FromList<L> = L extends Container<infer T>[] 
  ? L extends [Container<infer H>, ...infer Tail] 
    ? H | FromList<Tail> 
    : T 
  : never;

const f = <T extends Container<unknown>>(
  containers: T[],
  callback: (...values: FromList<typeof containers>[]) => void
) => { ... }
```

I know, I know, a fully fledged display of unnecessary complexity. That's exactly why I said _"amazing"_...

This `FromList` type says that, given a `List` of type `List of Containers of something`, infer the type of this `something`. The second validation says that, for this same given type `List of Containers of something` destructure it as a [Tuple](https://tsplay.dev/NBj34N).

Since _tuples_ are only lists with fixed size and known types (Weirdly close to a indexed type, don't you think so?), you can ask for this type expansion to infer the type of the first `something` within the `Container` type at the first index of this tuple while also using the spread operator to create a `Tail` with the remaining types.

If the first validation fails, it will return a `never`, a type that could not be constructed. In case it succeeds we enter the second validation. For the second validation if it fails we will just return the type of the first validation because this isn't a "tuple" it's exactly a `List of Container of something` and we just want this `something` to be our type. In case it succeeds, we will recursively create a `Union` which consists of the inferred type of the first `something` or the reduced type of subsequent `FromList` called with the inferred `Tail` type.

## That's not quite right, isn't it?

If you have spent some time playing with those custom utility types in TS then you're probably aware of the problem already.

Adding this same logic to the playground we can see that first of all, TS is not happy. He's saying;

```Typescript
const values: unknown[]
Argument of type 'unknown' is not assignable to parameter of type 'FromList<T[]>'.
```

That's because we didn't gave a type to the `values` array which is being filled and used as parameter for our `callback` function. And since we typed our `containers` parameter as `T[]` and `T` is extending a `Container of unknown`, we pretty much have no way to type it correctly without using type coercions which I tend to only use on cases where I have no alternatives.

The second problem is that we're returning a `Union type` from our `FromList` and that will be the type of each and every single one of our parameters within the `callback`.

```Typescript
// value1 is of type string | number
// value2 is of type string | number
// Any value within the callback will be this union type...
f([container1, container2], (value1, value2) => {
  ...
})
```

We need a way to know exactly which are the types that are being held by every `Container` in every index of our `containers` parameter.

## Removing the Complexity

First I want to mention that even though I've spent some time and have also used the destructured tuple in my first complex idea, I wasn't really able to move forward from there. 

After this `Union` type I knew that an indexed type was necessary so it would be easier to retrieve the `something` within each `Container` for the given `key` but I wasn't able to translate it into the correct idea. Lack of experience in a case like that I assume.

I went back to the same tweet to check how people where implementing or at least how they where thinking about the problem. Found some interesting points of view but the one that caught my attention was [this one](https://tsplay.dev/w24Rxm) from [devanshj__](https://twitter.com/devanshj__).

Mainly because of this line;
```Typescript
// Changing the nomenclature for consistency
declare const f:
  <T extends Container<unknown>[]>
    ( containers: [...T],
      ...) => { ... }
```

If you destructure the `T` as a tuple as the type for the `containers` parameter, TS will be able to infer which are the types in each and every index of it.

Who knows why but my head wasn't capable of thinking in use the tuple this way. So, I went and updated my utility custom type to the same of the above example.

```Typescript
type PickFromContainer<C> = {[K in keyof C]: C[K] extends Container<infer CT> ? CT : never};

const f = <T extends Container<unknown>[]>(
    containers: [...T],
    callback: (...values: PickFromContainer<T>) => void
) => { ... }
```

This `PickFromContainer` type says; given a type `C` (our tuple), for each of their keys (indexes), we will check if the type in that index is of type `Container of Something` and if it is, we will ask TS to infer this `something` for us and return it as the type within the index being mapped.

That's beautiful and works like a charm. If you place that on the playground you'll see that every param on the `callback` function is the same type `something` as the `Container of something` in that position based on the `containers` parameter order.

_But wait... Typescript is screaming at me again?_

```Typescript
Argument of type 'unknown[]' is not assignable to parameter of type 'PickFromContainer<T>'
```

Yeah, pretty much the same, since our type `T` is necessarily a `List of Containers of Unknown` and we're pushing a bunch of `Unknown` to `values`, it's  only fair that TS would consider the type of `values` as a `List of Unknown` and when we call our `callback` function with this list as a parameter, the `PickFromContainer` will not overlap the `List of Unknown`.

The only two things that can be assigned to `unknown` are `any` and `unknown` itself and that leaves us with two ways to make TS happy right now.

1. Be rude and say: "I don't care, it can be anything!" and Coerce the `values` list to `any`.
2. Be kind and say: "Hey Typescript, I know that `values` is `unknown` to you but I also know that it will be a `PickFromContainer` when needed".

From the start my idea was to remove `any`'s so I'm going with the second approach.

```Typescript
const f = <T extends Container<unknown>[]>(
    containers: [...T],
    callback: (...values: PickFromContainer<T>) => void
) => {
    const values = [] as unknown as PickFromContainer<T>;
    for (const container of containers) {
        values.push(container.value)
    }
    callback(...values)
}
```

That's not my favorite approach, as mentioned before. We could have used a different approach with type predicates and let the compiled part of JS optimize it on its own but let's keep it simple and tell TS to believe us in this case. [Here's the playground](https://www.typescriptlang.org/play?#code/C4TwDgpgBAwg9gO2AQwJYIgJwDwBUB8UAvFAN5QBuyANgK4QBcUuUAvgNwBQnAxogM7AoPTBGTAI8JGgyZiUPABp8ACip1GzAJRMpKdFjyEihUpygWoo4LUwIylGvTadW3UJCgAFVDwDWAGKYcAC2ejKGMMZkANoA0lDoUH4QIHAAZrAAurrxWVAQAB4SCAAm-LCI+rLY6OlYsARQAPyNUEwYFFgc3HwIglCZJHgFxRBlFeEGOLQIfghwAO4I+DFZquaWfdVY-EwxAHRHuFmKmxY8NNQARsj+TCpHB+r0e96+gcFhVRE4BFrEQgUOCoUqcAEmMjnYQCIQvCAVEhrKDICqzeZLeyo97+IKhKY1AhcSyDOByFR9AbbX5QDIw6TTfgAswkknw-gHMC0fgACwpP2mzycEC00LcJMu1Budz8jyO7NFbl4sPpO0wAEZ5CIxBICVgVAByHmoA2iylCanTABMWtE4kkAtkKitos4AHo3VAABJLKClOBQACSVggYGod2gwB5cH40GQCBAUA8CMSWKgi2Qiaj4nTqGoUAA5qgulAQtAinchMmKvDNfHSo4NFbFLmo3BaEIecgKOgC0mA-xIDxUOkszyICFmpx0ioYpbZOqW-OsFbTlA1MLF436C7AVDXFogA).

Typescript doesn't really need to be complicated - Even though it is in some cases. Sometimes we add unnecessary complexity to things and everyone have done it at least once.

Look at what other developers ideas and implementations are, its a really good way to improve yours. Just don't copy and paste everything, try to understand the _why_'s and _when_'s because that way you'll be able to improve and derive from it when needed.