Typescript interface for array of anything but other arrays

602 Views Asked by At

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. I started writing it thinking it would be simple, but then I stuck with describing an array of anything but other arrays.

interface Flatten {
  (any[]): ...?
}

Any ideas are welcome, thank you :))

2

There are 2 best solutions below

3
On

If you know all the data types flat array should return, you may define it like below:

type Flat = string|number|FlatObj;
type FlatArr = Array<Flat>;
type FlatObj = {[key: string]: Flat|FlatArr};

In your example you had nested empty object, so I assume you want it to contain only flat arrays too, FlatObj is definition of it.

Now if you try to assign nested arrays in any FlatArr variable, it should complain about it.

Link to playground

If you are looking for a Type Guard, here one example for FlatArr and FlatObj:

type AnyObj = {[key: string]: any};

function isFlatObj(obj: AnyObj): obj is FlatObj {
  let flat = true;
  for (const key in obj) {
    if (Array.isArray(obj[key]) && !isFlatArr(obj[key])) flat = false;
    else if (typeof obj[key] === 'object' && !isFlatObj(obj[key])) flat = false;
  }
  return flat;
}

function isFlatArr(arr: any[]): arr is FlatArr {
  let flat = true;
  arr.map((item) => {
    if (Array.isArray(item)) flat = false;
    else if (typeof item === 'object' && !isFlatObj(item)) flat = false;
  });
  return flat;
}

To make FlatArr, you can define a function for it. I made two functions below. One makes Arrays flat, the other makes sure object arrays are flat too:

const flattenArr = (arr: any[]): FlatArr => {
  const flatArr: FlatArr = [];
  for (const item of arr) {
    if (Array.isArray(item)) flatArr.push(...flattenArr(item));
    else if (typeof item === 'string') flatArr.push(item);
    else if (typeof item === 'number') flatArr.push(item);
    else if (typeof item === 'object') flatArr.push(flattenObj(item));
  }
  return flatArr;
}

const flattenObj = (obj: AnyObj): FlatObj => {
  let flatObj: FlatObj = {};
  for (const key in obj) {
    if (Array.isArray(obj[key])) flatObj[key] = flattenArr(obj[key]);
    else if (typeof obj[key] === 'string') flatObj[key] = obj[key];
    else if (typeof obj[key] === 'number') flatObj[key] = obj[key];
    else if (typeof obj[key] === 'object') flatObj[key] = flattenObj(obj[key]);
  }
  return flatObj;
}
4
On

It is doable in TS 4.5 (nightly version) but not in a way you expect.

Thanks to variadic-tuple-types you can do this:


type Reducer<
  Arr extends Array<unknown>,
  Result extends Array<unknown> = []
  > =
  (Arr extends []
    ? Result
    : (Arr extends [infer H, ...infer Tail]
      ? (H extends Array<any>
        ? Reducer<[...H, ...Tail], Result> : Reducer<Tail, [...Result, H]>) : never
    )
  )

// [1,2,3]
type Result = Reducer<[[[1], [[[[[[[2]]]]]]]], 3]> 

// [1, 2, 3, 4, 5, 6]
type Result2 = Reducer<[[[[[[[[[1]]]]]]]],[[[[[[2,3,4]]]],[[[[5,6]]]]]]]> 

How do I use that as a function return value type?

In order to use it with function, you need to convert argument to immutable array:

type Reducer<
  Arr,
  Result extends ReadonlyArray<unknown> = []
  > = Arr extends ReadonlyArray<unknown> ?
  (Arr extends readonly []
    ? Result
    : (Arr extends readonly [infer H, ...infer Tail]
      ? (H extends ReadonlyArray<any>
        ? Reducer<readonly [...H, ...Tail], Result> : Reducer<Tail, readonly [...Result, H]>) : never
    )
  ) : never


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


const result = flatten([[[[[[1]]], 2], 3]] as const)

Playground

You should have also add second argument to reduce method.

More explanation you can find in my article.

If you want to better understand how Reducer utility type work, see this exmaple:

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])
}