I'm trying to create a typed comparator function with type safety so that I'm sure that I'm always sorting with existing values So far I have done the following:
export enum SortDirection {
ASCENDING = "ASCENDING",
DESCENDING = "DESCENDING",
}
/**
* Sorts in place Array of objects
* @param path path to numeric value
* @param direction the way that numeric values will be evaluated
* @returns the sorted array
*/
export const sortByNumeric = <T>(path: Array<keyof T>, direction = SortDirection.ASCENDING) => <K extends Record<keyof T, any>>(current: K, next: K) => {
const extractedA = path.reduce((acc, k) => acc[k], current);
const extractedB = path.reduce((acc, k) => acc[k], next);
return direction === SortDirection.ASCENDING ? extractedA - extractedB : extractedB - extractedA;
}
let source = [
{
name: 'Jon',
age: 20,
father: {
name: 'Jon Senior',
age: 60,
otherProp: 100
}
},
{
name: 'Gabriel',
age: 25,
father: {
name: 'Jon Senior',
age: 80,
otherProp: 20
}
}
];
source.sort(sortByNumeric(['age']));
The function is working fine with a single property, but it is not able to work with nested paths like so :
source.sort(sortByNumeric(['father' , 'otherProp']));
// Here this will throw an error that the prop doesn't exits, while this is actually a valid property
or
source.sort(sortByNumeric(['age' , 'father']));
// Here there will be no error, while at the same time this is invalid value as there is no key for the 'father'
I'm sure that I'm doing something wrong with the generics, but I have no idea how and what to fix here, any guidance will be appreciated.
CodePudding user response:
It's possible, but not simple to type arbitrarily nested keys. It depends on how exactly you want the type to behave but a possible implementation of such a type would be:
export type NumericKeyPath<T> =
[T] extends [number] ? []
: [T] extends [object] ? NumericKeyPathProperty<T, keyof T>
: never;
type NumericKeyPathProperty<T, Key extends keyof T> =
Key extends (string | number) ? [ Key, ...NumericKeyPath<T[Key]> ]
: never;
type Test = NumericKeyPath<{ name: string; age: number; father: { name: string; age: number; otherProp: number } }>;
// type Test = ["age"] | ["father", "age"] | ["father", "otherProp"]
Used with your comparator function:
export const sortByNumeric = <T>(path: NumericKeyPath<T>, direction = SortDirection.ASCENDING) => (current: T, next: T) => {
// the below casts could be done more precisely
const extractedA = (path as string[]).reduce((acc: any, k) => acc[k], current);
const extractedB = (path as string[]).reduce((acc: any, k) => acc[k], next);
return direction === SortDirection.ASCENDING ? extractedA - extractedB : extractedB - extractedA;
}
let source = [
{
name: 'Jon',
age: 20,
father: {
name: 'Jon Senior',
age: 60,
otherProp: 100
}
},
{
name: 'Gabriel',
age: 25,
father: {
name: 'Jon Senior',
age: 80,
otherProp: 20
}
}
];
source.sort(sortByNumeric(['age']));
// Works
source.sort(sortByNumeric(['father' , 'otherProp']));
// Works
source.sort(sortByNumeric(['age' , 'father']));
// Error
Note that since it seemed to be the intention of the function, I wrote NumericKeyPath to only permit paths to numeric values. The relevant line is [T] extends [number] ? []
, for example if you would like to permit string values as well you could change this to [T] extends [number | string] ? []
.