Reputation: 83818
Given:
function sorter<T extends string | number> () {
const icf = new Intl.Collator().compare
return (a: T, b: T) => typeof a === 'number' ? a - b : icf(a, b)
}
We get a Typescript type error:
We know that if a
is a number then b
will also be a number, and similarly if a
is a string then b
will also be a string. This information is not known by the function calling sorter
i.e. only at the time the function being returned is called. By design we never expect to get heterogeneous items.
What options are there to assert these types correctly?
Upvotes: 2
Views: 757
Reputation: 771
Your issue is that the restriction on the generic type T extends string | number
does not enforce a
and b
having the same primitive type in (a: T, b: T) => ...
. One can be a string and the other a number, if T
is itself the union type string | number
.
For example:
// this compiles fine, even though the parameters have different types
const a: number | string = 2;
const b: number | string = '2';
sorter("en-us")(a, b);
Further, you've made the sorter
function generic, meaning that the type of the comparator function's parameters is chosen at the point in time where sorter
is called. In reality, it should be chosen by the caller of the function returned by sorter()
. If we make this anonymous function generic by returning <T,>(a: T, b: T) => typeof ...
we run into the same issue: this permits the caller to choose string | number
for the type T
and pass in a string and a number for comparison.
It is a bit ugly, but we can get the desired behaviour by instead returning a function that has declared overloads only for (string, string) => number
and (number, number) => number
. We need a single type assertion, but it is due only to a limitation of type inference in function overloads:
export function sorter(locale: string) {
const icf = new Intl.Collator(locale).compare
function comp(a: number, b: number): number;
function comp(a: string, b: string): number;
function comp(a: number | string, b: number | string) : number {
if (typeof a === "number") {
// assertion needed here: TS can't infer one param's type from another's via
// their relationship in overloads
// (but it is safe: this fn can only be called via a declared overload)
return a - (b as number);
} else {
return icf(a, b as string);
}
}
return comp;
}
Upvotes: 1
Reputation: 661
You could do something like this:
type T = number | string;
function numSorter(a: number, b: number){
return a - b;
}
function stringSorter(a: string, b: string, locale: string) {
const icf = new Intl.Collator(locale).compare;
return icf(a,b)
}
function picker(a: T, b: T, locale: string) {
if (typeof a === 'number' && typeof b === 'number') {
return numSorter(a,b);
} else if (typeof a === 'string' && typeof b === 'string') {
return stringSorter(a,b, locale);
} else {
throw Error("incompatible types");
}
}
function sorter(locale: string) {
return (a:T, b:T) => picker(a,b,locale);
}
const mySorter = sorter("US");
console.log(mySorter(1,2)); //both numbers, so ok
console.log(mySorter("world","hello")); // both strings, so ok
console.log(mySorter(1,"hello")); // incompatible types
Which gives an output like this:
I could be mistaken, but I think that this kind of approach defeats the purpose of static type checking. The generic reference still has to have a concrete implementation.
In this case, the operations you are performing are different for the different types number
and string
, so theres no overlap. The generic approach might be useful for objects that do have some overlap in functionality, like a common method they both have, like if you wanted a helper method for finding length of an array (as shown in the docs):
function loggingIdentity<Type>(arg: Array<Type>): Array<Type> {
console.log(arg.length); // Array has a .length, so no more error
return arg;
}
This generic function will work for any array of any type, because any array has the length
property.
Overall, I don't think its useful for this case, I think it might be better to have concrete implementations from the start, and calling the correct sorting function when needed. Generics are a bit of a headache to be honest, especially in TypeScript.
Upvotes: 1