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

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service and acknowledge you have read our privacy policy.

Start asking to get answers

Find the answer to your question by asking.

Ask question

Explore related questions

See similar questions with these tags.