1

I want to make a function like

function sortByKey<T>(items: T[], key: string): T[] {
  return items.sort((a, b) => a[key] - b[key]);
}

I need T[key] to be a number, but I'm not sure how to express that.

If I knew the key ahead of time I could obviously just do {key: number} but that doesn't work here.

I tried something like sortByKey<K>(items: {[k: K]: number}[], key: K) but that gives the error "An index signature parameter type cannot be a literal type or generic type. Consider using a mapped object type instead." I looked into mapped types but they don't seem to do what I need. Is something like this possible in TypeScript?

3
  • Why wouldn't T extends { [x: string] : number } not work for you? Generics can be a part of index signature only if the signature is a template literal, and even that is a very recent addition to the language Commented Jan 7, 2022 at 19:07
  • @OlegValter that works if the object has only fields that are numbers, right? but what if T is like {i: number, name: string}? Commented Jan 7, 2022 at 19:10
  • Well, why not use a union then? :) or combine known props and an index signature? The gist of the idea is to use an index signature as a constraint for the generic type parameter Commented Jan 7, 2022 at 19:13

1 Answer 1

4

You probably want the function to be generic both in T, the type of the items elements, and in K, the type of key. You can constrain K to be keylike (K extends PropertyKey) and constrain T to be a type with a number value at key K (T extends Record<K, number> using the Record<K, V> utility type):

function sortByKey<K extends PropertyKey, T extends Record<K, number>>(
  items: T[], 
  key: K
): T[] {
  return items.sort((a, b) => a[key] - b[key]);
}

You can't write {[k: K]: number} because that's an index signature which can't be generic. But you can write {[P in K]: number} using a mapped type. Mapped types are similar to but distinct from index signatures; see this answer for more information. Anyway, Record<K, number> is an alias for {[P in K]: number}, so you were getting close.

Playground link to code

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

2 Comments

Thanks, that works! I incorrectly assumed T extends Record<K, number> meant that T could only have keys of type K and they all had to be numbers
A lot of people seem to think that TypeScript object types are "exact" or "sealed", but they're not. If A and B are object types and A extends B, it means that A must have all the known keys from B, but it doesn't mean that A can't have more. Indeed it would be a big problem if you couldn't add properties via extension like interface Foo extends Bar { baz: string }. See this for more info

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.