DEV Community


Posted on • Originally published at

How to flatten a tuple type in TypeScript?

Every time when I need to deal with type conversion/mapping, I'm asking my self: How would I do it in pure functional language wich supports match. I mean match like in F#, ReasonML or Haskell.

Please keep in mind, that you are allowed to use only destruction and recursion. There is no filter, fold, map and other fancy methods in type system.

Wait, why am I even writing about functional languages? TypeScript is not strictly functional but, I believe, that type system is, or at least has a lot in common. For instance, you are unable to mutate type itself, you can only override it.

The function I'd like to describe is written in a little bit weird style as for typescript/javascript eco system but this is not the case.

Consider this example:

const Reducer = <T,>(
  Arr: ReadonlyArray<T>,
  Result: ReadonlyArray<T> = []
  : ReadonlyArray<T> => {

  if (Arr.length === 0) {
    return Result
  const [Head, ...Tail] = Arr;

  if (Array.isArray(Head)) {
    return Reducer([...Head, ...Tail], Result)

  return Reducer(Tail, [...Result, Head])
Enter fullscreen mode Exit fullscreen mode

This is function works in a way we expect. It accepts [[[[[1]]]]] and returns [1].

Above function should not be hard to understand. Since we already now how to flat the tuple, let's try to make it in type system.

It works only with TS nightly

type Reducer<
  Arr extends ReadonlyArray<unknown>,
  Result extends ReadonlyArray<unknown> = []
  > =
  // if Arr is empty -> return Result
  (Arr extends readonly []
    ? Result
    // if Arr is not empty - destruct it 
    : (Arr extends readonly [infer Head, ...infer Tail]
      // check if Head is an Array
      ? (Head extends ReadonlyArray<any>
        // if it is -> call Reducer with flat Head and Tail
        ? Reducer<readonly [...Head, ...Tail], Result>
        // otherwise call Reducer with Head without flattening
        : Reducer<Tail, readonly [...Result, Head]>
      ) : never
Enter fullscreen mode Exit fullscreen mode

As you might have noticed, I have used same algorithm as we used previously.

I just wanted to say, that if you want to write non trivial types, you need to change they way you are thinking.

I mean, if you learn some basics of F#, OCaml or any other functional language you will become better TypeScript programmer.

Let's go back to our utility type. You might want to ask me why I have used ReadonlyArray instead of regular Arrays?

This is because with ReadonlyArray it is easier to infer deep nested tuple.

See this example:

const flatten = <
  T extends ReadonlyArray<T | Elem>
>(arr: readonly [...T]): Reducer<T> =>
  arr.reduce((acc, elem) =>
      ? flatten(elem) as Reducer<T>
      : [...acc, elem] as Reducer<T>,
    [] as Reducer<T>

// readonly [1, 2, 3]
const result = flatten([[[[[[1]]], 2], 3]] as const)
Enter fullscreen mode Exit fullscreen mode

Please see my answer on stackoverflow

flatten takes an array of any data type and produces an array with every nested array flatten.

For example, [{}, 'hello', 2, [3, ['ay'], 'oi oi'] becomes [{}, 'hello', 2, 3, 'ay', 'oi oi'], and [[[[5]]]] becomes [5].

I need an interface that would describe such a function…

The end.

Discussion (0)