2

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)?

3
  • 1
    How strongly typed do you need the output to be? Please give examples of inputs and the desired types of the outputs. Depending how crazy you get, zip([1,2,3],["a", "b"]) could produce a value of type anywhere from the loose (number | string)[][] to the more reasonable [number | undefined, string | undefined][] to the incredibly specific [[1, "a"],[2, "b"],[3, undefined]]. Without more clarity in the question, this could have many possible answers. Commented Jun 16, 2022 at 15:13
  • Here would be a solution for a zip function return type: stackoverflow.com/a/70192772/8613630 Commented Jun 16, 2022 at 15:43
  • Are you looking for smth like this ? Commented Jun 16, 2022 at 18:22

1 Answer 1

4

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

Sign up to request clarification or add additional context in comments.

2 Comments

Wow, what a wonderfully detailed answer. Looks like I have some homework to do, figuring out how all of these types work! Thank you.
I think the first solution does not support readonly objects: const arrs = unzip([[1, "a"], [2, "b"], [3, "c"]] as ReadonlyArray<readonly [number,string]>)

Start asking to get answers

Find the answer to your question by asking.

Ask question

Explore related questions

See similar questions with these tags.