1

Basically I am trying to write a function which returns a sorter function for a specified string property of a given type.

sortString<User>('name')

So far I have the following :

type KeysAssignableToType<O, T> = {
    [K in keyof O]-?: O[K] extends T ? K : never;
}[keyof O];

export const sortString = <T>(field: KeysAssignableToType<T, string | undefined>) => (a: T, b: T): number => {
    if (a[field]) {
        return b[field] ? a[field].localeCompare(b[field]) : 1
    }
    return b[field] ? -1 : 0
}

But there is the following problem. TS gives me a TS2339: Property 'localeCompare' does not exist on type 'T[KeysAssignableToType ]'

Any ideas?

Thanks!

5
  • 1
    KeysAssignableToType<T, string> works only for outter scope, not for inner. Current version of TS is unable to figure out that a[field] is a string type if condition evaluates to true. Consider this workaround Commented Jun 2, 2022 at 16:15
  • 1
    Your two problems are two different things; could you please pick one of them to be the focus of your question? The optional property thing seems like you should just be writing KeysAssignableToType<T, string | undefined>, whereas the other one has to do with type inference and can be addressed by adding a constraint the compiler understand like this. Which one is the real question here? Commented Jun 2, 2022 at 16:54
  • @jcalz the first (a) problem, I figured out the solution to the optional property issue shortly after posting. Thanks for the suggestion! Commented Jun 2, 2022 at 17:45
  • So could you edit the question to remove references to the second problem? And then someone can post answers to the main question Commented Jun 2, 2022 at 17:56
  • Seems like a duplicate of this problem which I had a while ago. A solution is to declare the type parameter as <T extends Record<KeysAssignableToType<T, string | undefined>, string | undefined>> instead of just <T>. Ugly, but it works. (Note that you'll still get problems because of the undefined, which you can solve by declaring const aField = a[field], bField = b[field]; and using those variables instead of repeated indexed accesses.) Commented Jun 2, 2022 at 18:53

1 Answer 1

1

This is currently a limitation or missing feature of TypeScript. There's no type operator that behaves like KeysAssignableToType<O, T> where the compiler can understand that O[KeysAssignableToType<O, T>] is assignable to T when O or T are generic. The sort of higher-order logic encoded there just isn't available to the compiler. There's an open feature request at microsoft/TypeScript#48992 to support such a type operator. Until and unless this is implemented, all we have are workarounds.

One workaround is to move (or add) the constraint so that instead of (or in addition to) saying that a key type K is constrained to KeysAssignableToType<O, T>, we say that the object type O is constrained to something like Record<K, T>. The compiler will let you index into a Record<K, T> with a key K, even if K and T are generic. For example:

type ObjWithKeys<O, T> =
    { [P in KeysAssignableToType<O, T>]?: T };

export const sortString = <T extends ObjWithKeys<T, string | undefined>>(
    field: KeysAssignableToType<T, string | undefined>) => (a: T, b: T): number => {
        const aField = a[field];
        const bField = b[field];
        if (aField) {
            return bField ? aField.localeCompare(bField) : 1
        }
        return bField ? -1 : 0
    }

This works because we've constrained T to the equivalent to Record<KeysAssignableToType<O, T>, T> (although I added the optional modifier so that it doesn't have a problem with optional properties). So when we index into a or b with field, the compiler knows that the result will be assignable to string | undefined.

Please note that I copied a[field] and b[field] into their own variables aField and bField before narrowing them with truthiness checks (side note: you really want undefined and "" to be equivalent in sort order? okay I guess). That's due to a bug/limitation mentioned at microsoft/TypeScript#10530; the compiler isn't able to narrow properties if the property index isn't a simple literal type, and field is certainly not of a simple type. By using a separate variable, we can forget about property indices completely and focus on a single value.

Anyway, now it all works as desired:

interface User {
    name: string,
    optionalProp?: string,
    age: number
}

sortString<User>('name') // ok
sortString<User>('optionalProp') // ok
sortString<User>('age') // error

Playground link to code

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

Comments

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.