Kevin
Kevin

Reputation: 41

Does TypeScript have difficulty using Generics for indexing?

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

Answers (1)

Alex Wayne
Alex Wayne

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.

Playground

Upvotes: 5

Related Questions