1

I'm wondering if you could help me, I'd like to have a simple function to sort array of objects case-insensitive, example of usage:

myArray.sort((a, b) => sortArr(a, b, 'someKey'))

Function itself is dead simple:

const sortArr = (
  firstItem,
  secondItem,
  key
) => (firstItem[key].toLowerCase() > secondItem[key].toLowerCase() ? -1 : 1)

However I have a big trouble typing it. firstItem and secondItem should be generics (T). key should be a valid key of that object so I assume K extends keyof T is enough. However inside the function firstItem[key] when I try to use toLowerCase method says:

Property 'toLowerCase' does not exist on type 'T[K]'.

How do I type that T[K] where K is a single key of that object given as parameter is a string type?

jcalz
  • 264,269
  • 27
  • 359
  • 360
adammo
  • 171
  • 10
  • Does [this approach](https://tsplay.dev/NB4KgW) meet your needs? Please test it and get back to me. – jcalz Jan 04 '23 at 16:23
  • @jcalz Wow, that's pretty simple but the fact that it still suggests "someThirdKey" is a little annoying. Is there any trick to make that disappear as well (other than making a type that filters for only string keys)? – kelsny Jan 04 '23 at 16:32
  • @vera. "it still suggests 'someThirdKey'" I don't see that, could you demonstrate? If you care about IntelliSense suggesting keys based on keys of `firstItem` and `secondItem`, though, then I don't understand why you'd want something "other than making a type that filters for only string keys", which is [the way I would do it](https://tsplay.dev/mAd1BN). – jcalz Jan 04 '23 at 16:39
  • @jcalz do you have any resource where this is explained? `type KeysMatching = keyof { [K in keyof T as T[K] extends V ? K : never]: any }` – Konrad Jan 04 '23 at 16:56
  • 1
    @Konrad https://stackoverflow.com/q/54520676/2887218 has several implementations of the equivalent of `KeysMatching`. I'm not sure this is in scope for the question as asked, although I guess I'm waiting to hear back from the OP – jcalz Jan 04 '23 at 16:59
  • @jcalz first solution works flawlessly, HOWEVER our project has some limitations and I cannot use Record, it autoformats Record to { [key: K]: string } and says following: An index signature parameter type cannot be a literal type or generic type. Consider using a mapped object type instead. – adammo Jan 04 '23 at 17:14
  • 1
    ? "autoformats"? That seems out of scope of the question as asked, right? Some strange project build step that turns valid code into invalid code? You can replace `Record` with `{[P in K]: string}` unless that also gets "autoformatted" to something invalid. I'll write up an answer but I'm not going to worry about `Record` because that's a separate issue. – jcalz Jan 04 '23 at 17:42

1 Answers1

0

The most straightforward approach is to make sortArr() generic in the type K corresponding to the key parameter, and express your firstItem and secondItem parameter types in terms of it, as Record<K, string> using the Record<K, V> utility type (or the equivalent {[P in K]: string}):

const sortArr = <K extends PropertyKey>(
  firstItem: Record<K, string>,
  secondItem: Record<K, string>,
  key: K
) => (firstItem[key].toLowerCase() > secondItem[key].toLowerCase() ? -1 : 1);

That compiles with no error since the compiler understands that firstItem[key] and secondItem[key] are of type string (i.e., Record<K, string>[K] is assignable to string).

Let's make sure it works when you call it:

declare const myArray: Array<{
  someKey: string,
  someOtherKey: number,
  someThirdKey: boolean
}>;

myArray.sort((a, b) => sortArr(a, b, 'someKey')); // okay

myArray.sort((a, b) => sortArr(a, b, 'someOtherKey')) // error!
// --------------------------> ~
// Type 'number' is not assignable to type 'string'

Looks good. The compiler accepts a and b when you pass in the "someKey" key, but rejects them when you pass in "someOtherKey", because the type of a.someOtherKey is number and not string.

Playground link to code

jcalz
  • 264,269
  • 27
  • 359
  • 360