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
KeysAssignableToType<T, string>works only for outter scope, not for inner. Current version of TS is unable to figure out thata[field]is a string type if condition evaluates totrue. Consider this workaroundKeysAssignableToType<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?<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 theundefined, which you can solve by declaringconst aField = a[field], bField = b[field];and using those variables instead of repeated indexed accesses.)