I am currently developing a versatile sorting module using TypeScript. This module will take in an array of data objects, along with a list of keys to sort by. Once the sorting process is complete, the array will be sorted based on the specified keys.
To start off, I have defined two types:
export type Comparer<T>: (a: T, b: T) => number;
export type SortKey<T> = {
key: keyof T,
order: 'ascending' | 'descending',
comparer: Comparer<T[keyof T]>
};
During testing, I encountered a problem with the above setup. Let's consider an example:
// Assuming numberComparer has the type (a: number, b: number) => number
import { numberComparer } from 'somewhere';
type Person = {
id: number;
name: string;
};
const sortKey: SortKey<Person> = {
key: 'id',
order: 'ascending',
comparer: numberComparer
};
This resulted in a TypeScript error stating that the comparer
property should be of type Comparer<string | number>
, but numberComparer
does not match this requirement. The issue arises because the type inference is unable to determine that since the value of key
is 'id'
, it should compare numbers due to id
being a numeric field.
To address this, I introduced version 2 of the type:
export type SortKey<T, K extends keyof T = keyof T> = {
key: K,
order: 'ascending' | 'descending',
comparer: Comparer<T[K]>
};
In this updated version, the example would function correctly if we explicitly specify K
. While I believe TypeScript should be able to infer this implicitly, for now we can rely on explicit declarations:
const sortKey: SortKey<Person, 'id'> = {
key: 'id',
order: 'ascending',
comparer: numberComparer
};
The error has now been resolved. However, a new challenge emerges when examining the generic sort
function:
export function sort<T>(data: T[], keys: SortKey<T>[]);
While implementing the function body does not result in errors, consumers of the function are restricted in usage. For instance, attempting the following leads to issues:
const myKeys: SortKey<Person>[] = [
{
key: 'id',
order: 'descending',
comparer: numberComparer
},
{
key: 'name',
order: 'ascending',
comparer: stringComparer
}
];
This results in the original TypeScript errors, where the provided comparer is specific to a certain type (number or string), yet the comparer
property accepts all potential keys within the Person
type.
A possible solution could involve creating a "master" comparer with the necessary type constraints, which would then handle parameter type-checking and delegate calls to specific comparers accordingly. Although effective, this approach may seem inelegant.
Therefore, I turn to my fellow TypeScript enthusiasts for any alternative solutions to this dilemma. Your insights are truly appreciated as always.