Home > Enterprise >  Type of zip and unzip array functions?
Type of zip and unzip array functions?

Time:06-17

I have zip and unzip functions like this:

function range(size: number, startAt: number = 0): ReadonlyArray<number> {
  return [...Array(size).keys()].map((i) => i   startAt);
}

function unzip<T extends unknown[]>(array: T[]) /* ??? */ {
  const maxLength = Math.max(...array.map((x) => x.length));

  return array.reduce(
    (acc, val) => {
      val.forEach((v, i) => acc[i].push(v));

      return acc;
    },
    range(maxLength).map(() => [])
  );
}

function zip<T extends unknown[]>(...arrays: T[]) /* ??? */ {
  const maxLength = Math.max(...arrays.map((x) => x.length));

  return range(maxLength).map((i) =>
    range(arrays.length).map((_, k) => arrays[k][i])
  );
}

I have no idea how to type the return types though. Is it even possible?

I think it would be nice if zip([1, 2, 3], ["a", "b"]) returned with a type of [number | undefined, string | undefined][]. It seems that there is an answer here as someone in the comments pointed out that does precisely this. Likewise, it would be nice if unzip([1, "a"], [2, "b"], [3, undefined]) returned a type of [number[], string[]] or maybe [number[], (string | undefined)[]] depending on whichever is easiest.

I am a typescript noob, so naturally I tried to find some types on the internet, but they all seem to go the "lazy" route and type returns as any.

If this is even possible, how would I type the return types of these two functions (as well as similar functions where you "invert" the type of an array)?

CodePudding user response:

I got a solution for unzip that does what you want.

function unzip<
  T extends [...{ [K in keyof S]: S[K] }][], S extends any[]
>(arr: [...T]): T[0] extends infer A 
  ? { [K in keyof A]: T[number][K & keyof T[number]][] } 
  : never 
{
  const maxLength = Math.max(...arr.map((x) => x.length));

  return arr.reduce(
    (acc: any, val) => {
      val.forEach((v, i) => acc[i].push(v));

      return acc;
    },
    range(maxLength).map(() => [])
  );
}

Let's see if it works:

const a = unzip([[1, "a"], [2, "b"], [3, "c"]])
//   ^? [number[], string[]]

const b = unzip([[1, "a", new Date()], [2, "b", new Date()], [3, "c", new Date()]] )
//   ^? [number[], string[], Date[]]

const c = unzip([[1, "a"], [2, "b"], [3, undefined]])
//   ^? [number[], (string | undefined)[]]

Playground


Here is an alternative solution that is fully compatible with the solution above. But for array literals, this will produce more accurate return types.

type ValidContent =
  | null
  | string
  | number
  | boolean
  | Array<JSON>
  | Date
  | undefined
  | {
    [prop: string]: ValidContent
  }

type UnzipReturn<T extends any[]> = T[0] extends infer A 
  ? { 
      [K in keyof A]: [...{
        [K2 in keyof T]: T[K2][K & keyof T[K2]]
      }] 
    } 
  : never

function unzip<
  T extends [...{[K in keyof S]: S[K]}][], S extends ValidContent[]
>(arr: [...T]): UnzipReturn<T> {
  const maxLength = Math.max(...arr.map((x) => x.length));

  return arr.reduce(
    (acc: any, val) => {
      val.forEach((v, i) => acc[i].push(v));

      return acc;
    },
    range(maxLength).map(() => [])
  );
}
const a = unzip([[1, "a"], [2, "b"], [3, "c"]])
//   ^? [[1, 2, 3], ["a", "b", "c"]]

const b = unzip([[1, "a", new Date()], [2, "b", new Date()], [3, "c", new Date()]] )
//   ^? [[1, 2, 3], ["a", "b", "c"], [Date, Date, Date]]

const c = unzip([[1, "a"], [2, "b"], [3, undefined]])
//   ^? [[1, 2, 3], ["a", "b", undefined]]

const d = unzip([[1, "a"], [2, "b"], [3, "c"]] as [number, string][])
//   ^? [number[], string[]]

Playground

  • Related