2

I am trying to create a function that can flatten nested arrays in typescript.

So far I have this:

function flattenArrayByKey<T, TProp extends keyof T>(array: T[], prop: TProp): T[TProp] {
    return array.reduce((arr: T[TProp], item: T) => [...arr, ...(item[prop] || [])], []);
}

The array.reduce in there does exactly what I want as is, but I can't get the generics to play along nicely with what I want. I think my problem is that item[prop] returns any since it has no way of inferring that item[prop] returns T[TProp].

What I'm aiming for is a function that can take this structure:

interface MyInterface {
    arrayProperty: string[];
    anotherArray: number[]
    someNumber: number;
}

const objectsWithNestedProperties: MyInterface[] = [
    {
        arrayProperty: ['hello', 'world'],
        anotherArray: [1, 2],
        someNumber: 1,
    },
    {
        arrayProperty: ['nice', 'to'],
        anotherArray: [3, 4],
        someNumber: 2,
    },
    {
        arrayProperty: ['meet', 'you'],
        anotherArray: [5, 6],
        someNumber: 3,
    },
];

and return an array that contains the contents of all the nested array.

const result = flattenArrayByKey(objectsWithNestedProperties, 'arrayProperty');

result should look like ['hello', 'world', 'nice', 'to', 'meet', 'you']

Basically I am looking for SelectMany from C#'s linq.

2
  • Array.flat() already works in some modern browsers
    – Kokodoko
    Commented Jun 20, 2019 at 17:48
  • @Kokodoko that doesn't let me specify which nested array I want to flatten.
    – ldam
    Commented Jun 24, 2019 at 8:42

2 Answers 2

2

Note: The following answer was tested on TS3.5 in --strict mode. Your mileage may vary if you use other versions or compiler flags.


How about this:

function flattenArrayByKey<K extends keyof any, V>(array: Record<K, V[]>[], prop: K): V[] {
    return array.reduce((arr, item) => [...arr, ...(item[prop] || [])], [] as V[]);
}

You have to tell the compiler that T[TProp] was going to be an array. Instead of trying to go that route, I have the generics be K (which you were calling TProp) and V, the element type of the array property at array[number][K]. Then you can type array as Record<K, V[]>[] instead of T[] (A Record<K, V[]> is an object whose property at key K is of type V[]). And it returns a V[].

Now the compiler understands what you're trying to do, although you do need to tell it that the initial empty array as the second parameter to reduce is supposed to be a V[] (hence [] as V[]).

And that should work how you want. Hope that helps; good luck!

Link to code

Update: the above seems not to infer well when the object has arrays of different types. You might find yourself having to type it explicitly like flattenArrayByKey<"anotherArray", number>(objectsWithNestedProperties, "anotherArray") which is redundant and annoying.

The following is a more complicated signature but it has better inference and better IntelliSense suggestions:

type ArrayKeys<T> = { [K in keyof T]: T[K] extends any[] ? K : never }[keyof T];

function flattenArrayByKey<T extends Record<K, any[]>, K extends ArrayKeys<T>>(
  array: T[],
  prop: K
): T[K][number][] {
  return array.reduce(
    (arr, item) => [...arr, ...(item[prop] || [])],
    [] as T[K][number][]
  );
}

It should behave the same in terms of the inputs and outputs, but if you start typing

const result = flattenArrayByKey(objectsWithNestedProperties, "");
// put cursor here and get intellisense prompts (ctrl-spc) ---^

It will suggest "arrayProperty" (and now "anotherArray") as the second parameter, since only arrayProperty (and "anotherArray") is suitable for reducing like that.

Hope that helps again. Good luck!

Link to code

8
  • The first one isn't accepting my array since it wants a record and I'm not quite understanding what record I'm supposed to be putting in there. The second one returns a union type of all possible arrays.
    – ldam
    Commented Jun 24, 2019 at 8:39
  • Can you provide a minimal reproducible example of what’s not working? I gave links to the code which do work for your example, so if something is not working I can’t reproduce it.
    – jcalz
    Commented Jun 24, 2019 at 13:11
  • I updated the OP with an extra array property, with it the return type is a union of ( string | number).
    – ldam
    Commented Jun 24, 2019 at 13:58
  • Hmmm, it is working on typescript playground but in vscode I get the union type. We are on TS 3.2.4, I wonder if the version has anything to do with it?
    – ldam
    Commented Jun 24, 2019 at 14:00
  • 1
    Yes, the second one might not work for 3.2 but the first one should (you can check in the playground by changing the version to... 3.1.6). The TypeScript language is still under fairly active development and has lots of compiler options, so unless someone mentions otherwise I think I've been assuming the newest version (3.5 right now) with all the --strict flags.
    – jcalz
    Commented Jun 24, 2019 at 14:15
0

Turns out ESNext has Array.flatMap which does exactly what I want.

https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Array/flatMap

Syntax

var new_array = arr.flatMap(function callback(currentValue[, index[, array]]) {
   // return element for new_array
}[, thisArg])

callback Function that produces an element of the new Array, taking three arguments:

currentValue The current element being processed in the array.

index (Optional) The index of the current element being processed in the array.

array (Optional) The array map was called upon.

thisArg (Optional) Value to use as this when executing callback.

To use it, I needed to add esnext.array to my lib in tsconfig.json:

{
    "compilerOptions": {
         "lib": ["es2018", "dom", "esnext.array"]
     }
}

It does exactly what I want:

objectsWithNestedProperties.flatMap(obj => obj.arrayProperty)
// returns ['hello', 'world', 'nice', 'to', 'meet', 'you']

NB: This isn't supported by IE, Edge, and Samsung Internet.

Not the answer you're looking for? Browse other questions tagged or ask your own question.