Home > database >  Trying to limit Typescript function to string properties of a given type
Trying to limit Typescript function to string properties of a given type

Time:06-03

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!

CodePudding user response:

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

  • Related