1

I have a generic javascript function that I'm having trouble typing correctly in typescript. It takes an array of items. If those items are arrays, it flattens them into a single array. If they are objects, it merges them into one big object. otherwise it just returns the original array.

function combineResults(responses) {
  if (responses.length === 0) {
    return [];
  }

  if (Array.isArray(responses[0])) {
    return responses.flat(1);
  } else if (typeof responses[0] === 'object') {
    return Object.assign({}, ...responses);
  }
  else {
    return responses;
  }
}

Is it possible to type this safely so that if you pass an array of arrays, your return type will be an array, and if you pass an array of objects, your return type will be an object. And if you pass an array of neither arrays or objects, your return type will be the original array type.

bdwain
  • 1,665
  • 16
  • 35

2 Answers2

2

I'd be inclined to give this an overloaded type signature corresponding to each case. There are a few snags here: one is that you're checking just the first element of responses and assuming the rest of the elements are of the same type; but arrays can be heterogeneous. And since arrays in JS and TS are considered objects, the call signature for combineResults() might do weird things if you give it a heterogenous array like [[1, 2, 3], {a: 1}]. I don't know what you want to happen at runtime there, so I don't know what you want to see happen in the type signature. These are edge cases.

Another snag is that an array like [{a: 1}, {b: ""}] is considered in TypeScript to be of type Array<{a: number, b?: undefined} | {b: string, a?: undefined}>, and to turn that into {a: number, b: string} involves a lot of type-system hoop-jumping, including turning unions to intersections and filtering out undefined properties.

So, here goes:

type UnionToIntersection<U> =
  (U extends any ? (k: U) => void : never) extends ((k: infer I) => void) ? I : never
type Defined<T> = T extends any ? Pick<T, { [K in keyof T]-?: T[K] extends undefined ? never : K }[keyof T]> : never;
type Expand<T> = T extends infer U ? { [K in keyof U]: U[K] } : never;

function combineResults<T extends ReadonlyArray<any>>(responses: ReadonlyArray<T>): T[number][];
function combineResults<T extends object>(responses: ReadonlyArray<T>): Expand<UnionToIntersection<Defined<T>>>;
function combineResults<T extends ReadonlyArray<any>>(responses: T): T;
function combineResults(responses: readonly any[]) {
  if (responses.length === 0) {
    return [];
  }
  if (Array.isArray(responses[0])) {
    return responses.flat(1);
  } else if (typeof responses[0] === 'object') {
    return Object.assign({}, ...responses);
  }
  else {
    return responses;
  }
}

The call signature should map arrays-of-arrays to an array, arrays-of-objects to an object, and any other array to itself. Let's test it:

const arrs = combineResults([[1, 2, 3], ["a", "b"]]); // (string | number)[]
const objs = combineResults([{ a: 1 }, { b: "hey" }]) // {a: number, b: string}
const nons = combineResults([1, 2, 3]); // number[]

Looks good, I think. Watch out for edge cases, though:

const hmm = combineResults([[1, 2, 3], { a: "" }])
/* const hmm: {
    [x: number]: number;
    a: string;
} ?!?!? */

You might want to tune those signatures to prevent heterogeneous arrays entirely. But that's another rabbit-hole I don't have time to go down right now.

Okay, hope that helps; good luck!

Playground link to code

jcalz
  • 264,269
  • 27
  • 359
  • 360
  • Wow thanks! This is great! It works really well. I ended up replacing a bunch of the any's with unknowns because of my tsconfig rules but that doesn't seem to cause problems. And i said the function itself had a return type of unknown since the actual types are handled by the overloads. – bdwain Jun 14 '20 at 06:23
0

Assuming the items are object, how about this:

function combineResults(responses: Array<object> | Array<Array<object>>): Array<object> | object {
remram
  • 4,805
  • 1
  • 29
  • 42
  • I can't assume they are objects. The goal is to handle an array of arrays, an array of objects, or an array of arbitrary items. And the return value would be an object if it was an array of objects, not an array. – bdwain Jun 13 '20 at 22:57
  • Then just change `object` to `any` or a generic type – remram Jun 13 '20 at 22:59