Reputation: 41
Expanding on an example from Effective Typescript I can't tell if I'm running into a limitation of the type checker or my understanding.
Here's the example in its raw form from page 33
interface Point {
x: number
y: number
}
function sortBy<K extends keyof T, T>(vals: T[], key: K): T[] {
// ...
}
const pts: Point[] = [
{ x: 1, y: 1 },
{ x: 2, y: 0 },
]
console.log(sortBy(pts, "x"))
Here's my attempt to replace //... with something useful
interface Point {
x: number
y: number
}
function sortBy<K extends keyof T, T>(vals: T[], key: K): T[] {
// my solution
return vals.sort((a, b) => {
return b[key] - a[key] // Error: The left-hand side of an arithmetic operation must be of type 'any', 'number', 'bigint' or an enum type.ts(2362)
})
// end my solution
}
const pts: Point[] = [
{ x: 1, y: 1 },
{ x: 2, y: 0 },
]
console.log(sortBy(pts, "x"))
But if I replace with
return (b[key] as any) - (a[key] as any)
it works
What I can't figure out is why does the type checker think a[key] or b[key] could be anything other than a number, since a and b must be type Point, and key must be keyof Point.
If I write this without generics it works fine, which makes me think something hinky is going on here.
function sortBy(vals: Point[], key: "x" | "y"): Point[] {
return vals.sort((a, b) => {
return b[key] - a[key]
})
}
Upvotes: 4
Views: 63
Reputation: 187034
A generic function or type must be valid internally, regardless of how it's called or used. To put in another way, sortBy
has no clue that Point
exists, or that it may eventually be called with that type. It needs to handle any array of object shapes that you may want to pass in, not just Point[]
.
If you look at this function type:
function sortBy<K extends keyof T, T>(vals: T[], key: K): T[] {
return vals.sort((a, b) => {
return b[key] - a[key]
})
}
The type number
does not appear, and yet you are doing operations that require numbers.
Here's the case it's complaining about:
sortBy([{ a: 'a string' }], 'a')
There's nothing in the above function signature type that says this is invalid. But when the function runs it will crash since string - string
is not a valid operation.
The fact you only pass it object with number properties doesn't matter because you could tell it too look up some other type instead.
The solution is to constrain the K
generic to only allow keys that would yield a type of number
.
function sortBy<K extends string, T extends Record<K, number>>(vals: T[], key: K): T[] {
return vals.sort((a, b) => {
return b[key] - a[key];
})
}
console.log(sortBy([{a: 'asd', b: 123}], "b")) // works
console.log(sortBy([{a: 'asd', b: 123}], "a")) // type error
// Type 'string' is not assignable to type 'number'.(2322)
Now in order to satisfy calling this function K
can be any string
, but the object must have a property that has a number
on the named property.
Upvotes: 5