Generics when alternating types in variable number of arguments to a function

1 week ago 6
ARTICLE AD BOX

In my code I frequently find myself doing complex sorting of large arrays of objects.

I've been working on a generic sorting function where you can provide a variable number of sorting comparators (and for each comparator you can add an optional parameter to specify the sort direction). I'm also somewhat new to generics so this has been a big learning experience.

Here's an example of how I'd like to use such a sorting function:

let arr = [{a: 1, b: 1, c: {d:1, e:1, f: {g:1}},....] sortBy( arr, // array to be sorted x => x.a, // first sort by the 'a' field default 'ascending' sort x => x.b, 'DESC', // next sort by the 'b' field 'descending' sort x => x.c.d, // next sort by the 'c.d' field default 'ascending' sort x => x.c.f.g, 'DESC') // next sort by the 'c.f.g' field 'descending' sort

Here's the code I have so far:

// RefToObjType and SortDirType are the two kinds of arguments // that the below "sortBy" function can accept type RefToObjType<refT> = (ref: refT) => any type SortDirType = 'ASC' | 'DESC' // TypeIsAorAB ensures that: // 1) the arguments array start with the "A" type // 2) that an argument of type "B" follows an argument of type "A" // meaning that you can't have multiple "B" arguments in a row type TypeIsAorAB<IsFirst, A, B, T extends any[]> = T extends [A, B, ...infer Rest] ? TypeIsAorAB<false, A, B, Rest> : T extends [A, ...infer Rest] ? TypeIsAorAB<false, A, B, Rest> : T extends [] & IsFirst ? false : T extends [] ? true : false; // the sorting function // the first parameter is the array to be sorted with type "refT[]" // the following args array are either the comparators or the sort direction function sortBy<refT, T extends (RefToObjType<refT> | SortDirType)[]>( arr: refT[], ...args: ( TypeIsAorAB<true, RefToObjType<refT>, SortDirType, T> extends true ? T : never ) ) { let compareFunctions: { refToObj: RefToObjType<refT> sortDir: SortDirType }[] = [] for (let i = 0; i < args.length; ) { let argI = args[i++] if (typeof argI == 'string') continue; let argII = args[i++] compareFunctions.push({ refToObj: argI, sortDir: argII != null && typeof argII == 'string' ? argII : 'ASC' }) } let sortFunc = function (refA: refT, refB: refT) { for (let compareFunction of compareFunctions) { let aLessThanB = compareFunction.sortDir == 'ASC' ? -1 : 1 let bLessThanA = -aLessThanB let objA = compareFunction.refToObj(refA) let objB = compareFunction.refToObj(refB) if (objA < objB) return aLessThanB if (objA > objB) return bLessThanA } return 0 } return arr.sort(sortFunc) }

There are 2 types of arguments that can be passed to the function (aside from the initial argument that contains the array to sort). RefToObjType<refT> is a function type and it returns the field to sort by. SortDirType is either 'ASC' or 'DESC' for ascending sorting or descending sorting. I want the sort direction to always be optional and to always come after a RefToObjType argument. So the type TypeIsAorAB enforces the order of the arguments.

The issue I'm having is that the sortBy function is requiring my to provide a type in all of the function arguments, and I was hoping those types would get inferred.

Here's what I want the function usage to look like, notice how all of the types are inferred by the arr parameter:

let arr = [{a: 1, b: 1},{a: 1, b: 2}] let a = sortBy(arr, x=>x.a, x=>x.b, 'DESC')

but for some reason it's requiring me to supply the type for each function like this which is extra code that makes it harder to read and I was expecting the types to get inferred and that's not happening:

type RefT = {a: number, b: number} let arr: RefT[] = [{a: 1, b: 1},{a: 1, b: 2}] let a = sortBy(arr, (x: RefT)=>x.a, (x: RefT)=>x.b, 'DESC')

I feel like this function is really close to working, but not sure how to get TypeScript to infer the types.

Read Entire Article