2
votes

I need to write generic function that takes as parameters object and key from subset of keys of object's type which correspond to the values of the specified type.

I tried to implement it as follows.

type KeysOfType<T, TProp> = { [P in keyof T]: T[P] extends TProp ? P : never }[keyof T];

function getLen<T>(obj: T, p: KeysOfType<T, string>): number {
    return obj[p].length
}

But the compiller gives an error with message "Property 'length' does on exist on type 'T[{ [P in keyof T]: T[P] extends TProp ? P : never }[keyof T]]'".

Why does the compiler does not consider that I have limited set of possible keys only keys corresponding to a value of type string? How to fix it?

1

1 Answers

2
votes

The compiler just isn't clever enough. Conditional types that depend on generic parameters (like KeysOfType<T, string>) are generally treated by the compiler as somewhat opaque, and while you understand that KeysOfType<T, V> was specifically constructed so as to make sure that T[KeysOfType<T, V>] extends V is true, the compiler doesn't even try.

The most general solution available to us in cases like this is to use a type assertion. For example, you can tell the compiler not to worry, and to treat obj[p] as a string:

function getLen<T>(obj: T, p: KeysOfType<T, string>): number {
    return (obj[p] as unknown as string).length;
    // the type T[{ [P in keyof T]: T[P] extends string ? P : never; }[keyof T]]
    // is so opaque to the compiler that we must widen to unknown 
    // before narrowing to string
}

Note that you are relieving the compiler of the duty of verifying type safety. You could just as easily had said obj[p] as unknown as boolean and the compiler would have believed you. So use this power with caution.


Another way to do a similar thing is to use a single function overload to distinguish between the generic conditional type as seen by the caller and the hopefully more tractable type as seen by the implementation:

// call signature, unchanged
function getLen<T>(obj: T, p: KeysOfType<T, string>): number;

// implementation signature... let's make p the generic type K
// and say that obj has keys K and values of type string
function getLen<K extends keyof any>(obj: Record<K, string>, p: K): number {
  return obj[p].length;
}

The reason that it's similar to a type assertion is because the compiler allows you to make the implementation signature looser than the call signatures... if you're not careful you can lie to the compiler and you won't see a problem until runtime.


Okay, hope that helps. Good luck!