I got a simple function with an array of objects. On this array there will always be at least one string property.
In the body I want to dinamically be able to select that string by key and order with localCompare.
The problem is that the properties can have numbers and when doing localCompare TS complains.
;(() => {
type entries = {
[key: string]: string | number
}
const data: entries[] = [
{ key: 'bbb', number: 1 },
{ key: 'aaa', number: 2}
]
// Let's imagine index comes as a parameter
const index = 'key'
const order: entries[] = data.sort((a, b) => a[index].localeCompare(b[index]))
console.log(order)
})()
Just make sure you always convert the properties to strings:
;(() => {
type entries = {
[key: string]: string | number
}
const data: entries[] = [
{ key: 2, number: 4 },
{ key: 'bbb', number: 1 },
{ key: 'aaa', number: 2},
{ key: 1, number: 3}
]
const index = 'key'
const order: entries[] = data.sort((a, b) =>
`${a[index]}`.localeCompare(`${b[index]}`))
console.log(order)
})()
Or, check the type of the values:
;(() => {
type entries = {
[key: string]: string | number
}
const data: entries[] = [
{ key: 'bbb', number: 1 },
{ key: 'aaa', number: 2}
]
const index = 'key'
const order: entries[] = data.sort((a, b) => {
const valA = a[index];
const valB = b[index];
return typeof valA === 'number' && typeof valB === 'number'
? valA - valB
: `${valA}`.localeCompare(`${valB}`);
});
console.log(order)
})()
Based on your comments, I think you're saying that you know that a[index] will always be a string because of logic in your code, but the problem is that TypeScript doesn't know that (because the object signature says it can be a string or a number).
To reassure TypeScript, you have at least a couple of choices:
function assertIsString(value: any): asserts value is string {
if (typeof value !== "string") {
throw new Error(`Expected a string, but got ${typeof value}`);
}
}
Then:
const order: entries[] = data.sort((a, b) => {
const avalue = a[index];
const bvalue = b[index];
assertIsString(avalue);
assertIsString(bvalue);
return avalue.localeCompare(bvalue);
});
This also has the advantage of giving you an explicit error if your code fails to ensure that the property name is only the name of a string property.
If you don't want the (very minimal) overhead of the assertion function, you can just override TypeScript:
const order: entries[] = data.sort((a, b) => (a[index] as string).localeCompare(b[index] as string));
If you know your objects are homogenous, you can combine the two approaches, for instance using the assertion function just on the first entry:
if (data.length > 1) {
assertIsString(data[0][index]);
}
const order: entries[] = data.sort((a, b) => (a[index] as string).localeCompare(b[index] as string));
That way you get a nice explicit error, but you don't have the overhead of checking during the sort.
Your TS doesn't follow the TS guidelines.
Here I made a function sortByProp, which uses keyof so you can easily select the property you want to use for your sort.
You can try the code here: Playground.
When I find out how to filter the types of properties, I will update the code again.
interface Entrie {
key: string;
number: number | string;
random?: boolean;
}
// Used so I can filter the keys by the type I want. In this case string | number;
type FilteredKeys<T, U> = { [P in keyof T]: T[P] extends U ? P : never }[keyof T];
const data: Entrie[] = [
{ key: 'bbb', number: 1 },
{ key: 'aaa', number: 3 },
{ key: 'aba', number: 2 }
];
function sortByProp<T, F>(arr: T[], prop: FilteredKeys<T, F>) {
if (typeof prop == 'string') return arr.sort((a, b) => {
if (a[prop] < b[prop]) return -1;
if (a[prop] > b[prop]) return 1;
return 0;
});
return arr.sort((a: any, b: any) => a[prop] - b[prop]);
}
console.log("SortByKey:", sortByProp<Entrie, string | number>(data, "key"));
console.log("SortByNumber:", sortByProp<Entrie, string | number>(data, "number"));