Let's suppose you need the alphabeticallyBy()
function to accept a rest parameter of keys, each pointing to a string property within your array elements. For example, considering the following array:
declare const arr: Array<
{ prop1: { prop2: { prop3: string; prop4: number }; prop5: boolean }; prop6: Date }
>;
You would expect this code to work because arr[i].prop1.prop2.prop3
is a number:
arr.sort(alphabeticallyBy("prop1", "prop2", "prop3")); // should be fine
However, this code should result in an error since arr[1].prop1.prop2
is not a number:
arr.sort(alphabeticallyBy("prop1", "prop2")); // error!
If that's the case, we can implement a recursive utility type called DeepRecord<K, V>
, where K
represents a tuple of property keys and V
is the value type at the specified path indicated by K
. Check out the example below:
type DeepRecord<K extends PropertyKey[], V> =
K extends [infer K0 extends PropertyKey, ...infer KR extends PropertyKey[]] ?
{ [P in K0]: DeepRecord<KR, V> } : V
This utilizes variadic tuple types and conditional type inference to extract the first key off the tuple and apply it recursively until reaching the base case. Now, instead of using Record<T, string>
, alphabeticallyBy()
can utilize DeepRecord<K, string>
:
declare function alphabeticallyBy<K extends PropertyKey[]>(...keys: K):
(a: DeepRecord<K, string>, b: DeepRecord<K, string>) => number;
Testing it out:
const s = alphabeticallyBy("prop1", "prop2", "prop3");
/* const s: (
a: { prop1: { prop2: { prop3: string; }; }; },
b: { prop1: { prop2: { prop3: string; }; }; }
) => number */
Everything seems to be working as expected. This setup also allows for zero keys to be passed, ensuring objects must contain only strings in such cases:
["a", "b", "c"].sort(alphabeticallyBy()) // okay
The implementation currently permits passing zero keys, but this behavior can be restricted if necessary by modifying the type constraint.
Playground link to code