Home > Net >  Generic function for sorting a nested array of objects
Generic function for sorting a nested array of objects

Time:07-15

I want to sort a nested array using a generic function. It should be sorted by the values of the items from the nested array.

My array:

type Person = {
    id: number,
    name: string,
    childs: Child[]
}


type Child = {
    id: number,
    name: string,
}

const persons : Person[] = [
    {
        id: 1, name: 'Person 1', 
        childs:[
            {id: 1, name: 'Child 1'},
            {id: 2, name: 'Child 2'}
        ]
    },
    {
        id: 2, name: 'Person 2', 
        childs:[
            {id: 1, name: 'Child 1'},
        ]
    },
        {
        id: 3, name: 'Person 3', 
        childs:[
            {id: 1, name: 'Child 1'},
            {id: 2, name: 'Child 2'}
            {id: 3, name: 'Child 3'}
        ]
    },  
];

As a result, I want to call my sort function like this:

sortNestedArrays(persons, 'childs', 'name');

That means: Sort the nested array 'childs' by the property 'name'.

My approach

I've been struggling with the correct syntax for this function for hours now.

type ArrayElement<ArrayType extends readonly unknown[]> = 
  ArrayType extends readonly (infer ElementType)[] ? ElementType : never;

type KeysMatching<T, V> = {[K in keyof T]-?: T[K] extends V ? K : never}[keyof T];

/**
 * @param array Main array.
 * @param keyOfSubArray Key of the elements in 'array' that leads to the value of the nested array.
 * @param propertyName Key by which the nested array is sorted.
 */
function sortNestedArrays<A, K extends KeysMatching<A, unknown[]>>(array: A[], keyOfSubArray: K, propertyName: ???){
    array.forEach((member) => {
        const nestedArray = member[keyOfSubArray];
        nestedArray.forEach((nestedMember) => {

        });
    });
}

This is my result so far. With KeysMatching I could achieve that the argument keyOfSubArray accepts only keys that are of type array, so that the nested array can be accessed safely.

However, Typescript does not understand the following lines. Since keyOfSubArray can only be a key that points to an array, it follows that member[keyOfSubArray must also be an array. However, the compiler throws the following error message: Property 'forEach' does not exist on type 'A[K]'.

const nestedArray = member[keyOfSubArray];
nestedArray.forEach((nestedMember) => {

});

My questions

1st) Why doesn't the compiler recognize that member[keyOfSubArray] must be an array and how can I solve this problem?

2nd) In the next step, I need to specify the argument propertyName which must be a key of the items in the subarray (keyof Child). How can I specify this correctly as generic in the function?

Playground

Typescript Playground

CodePudding user response:

To your first question: "Why doesn't the compiler recognize that member[keyOfSubArray] must be an array?". Because it has no reason to assume so. You specified that the array parameter passed to the function is of type A. You did not further constrain this generic type to be of any particular shape, so it could be anything.

You should constrain A with additional information.

function sortNestedArrays<
  A extends Record<K, unknown[]>, 
  K extends KeysMatching<A, unknown[]>
>(array: A[], keyOfSubArray: K, propertyName: ???){}

Now the compiler knows that A must have a property K which has an array type.


The second problem is of similar nature. You should tell the compiler that the type of the array inside A is also important by using another generic type P.

function sortNestedArrays<
  A extends Record<K, P[]>,
  K extends KeysMatching<A, unknown[]>,
  P
>(array: A[], keyOfSubArray: K, propertyName: keyof P){}

We will use P as the type of the elements inside the array in A and also for the propertyName parameter.

Playground


We can make it slightly shorter:

function sortNestedArrays<
  A extends Record<K, unknown[]>,
  K extends KeysMatching<A, unknown[]>
>(array: A[], keyOfSubArray: K, propertyName: keyof A[K][number]){
    array.forEach((member) => {
        const nestedArray = member[keyOfSubArray];
        nestedArray.forEach((nestedMember) => {

        });
    });
}

This will also fix the auto-completion issue.

CodePudding user response:

To build on Tobias' answer:

type ExtractFromArray<T extends any[]> = T extends (infer Type)[] ? Type : never;

/**
 * @param array Main array.
 * @param keyOfSubArray Key of the elements in 'array' that leads to the value of the nested array.
 * @param propertyName Key by which the nested array is sorted.
 */
function sortNestedArrays<
  A extends Record<K, C[]>,
  K extends KeysMatching<A, C[]>,
  C
>(array: A[], keyOfSubArray: K, propertyName: keyof ExtractFromArray<A[K]>){}

sortNestedArrays(persons, "childs", "id")

In this scenario you will have key suggestions for your last parameter because we explicitly specify that it must be a key of the combination of A[K] (your previous params) which is Child[] and then using the ExtractFromArray type to get the keys of Child and not Child[].

Test it in the playground.

  • Related