4

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]
  })
}

1 Answer 1

5

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.

Playground

Sign up to request clarification or add additional context in comments.

5 Comments

Hmm, yeah that's much better! I thought I tried that the other day and was getting errors about a missing type signature, though. Not sure what I was doing... I'll update the answer.
1) Is this a limitation of generics in typescript or generics in general? 2) Does this mean that example from the book is completely useless because in its current form there's no way to look inside T without typecasting? 3) If you call the function with nonkeyable value, like sortBy(pts, ["x"]), TS is smart enough to know "Type 'string[]' is not assignable to type '"y"'", which means the checking is looking and the entire scenario, including the structure of T, and not just blindly accepting the generic function as is, but how its being used too, in which case it should know T better
1) I wouldn't classify it as a limitation at all. You can't safely treat something as a number that isn't typed in a way to guarantee it's a number. Thats the point of typescript. 2) I don't have this book, but I would say it's not useless. What it shows is that you can constrain key to be a key of the member type of an array. But if you want to actually use any values you have to know what they are to know if you can do what you want to do with those values. I don't know the author's intent of the example exactly.
3) When you call the function, it checks that the arguments match the call signature. If it does not, it raises a type error. That's why sortBy(pts, ['x']) doesn't work. Because the array [x] is not a key of the members of pts. In the function itself, it checks to make sure that the operations you are doing are allowed by the type of the arguments. The point of a generic function is that you don't know exactly what types it its generic will be set to when they are called. And what types they are allowed to called with are set by your generic constraints.

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service and acknowledge you have read our privacy policy.

Start asking to get answers

Find the answer to your question by asking.

Ask question

Explore related questions

See similar questions with these tags.