TypeScript: Accept all Object keys that map to a specific type

If you want something that works from both the caller’s point of view and from the implementer’s point of view, you can do this:

function shouldOnlyAcceptStringValues<K extends PropertyKey>(
  o: Record<K, string>, key: K
) {
    const okay: string = o[key];
}

This is sort of looking at your constraint backwards; instead of constraining key to be the right keys from obj, you are constraining obj to be an object whose value type at key is a string. You can see that okay is accepted as a string, and things work from the caller’s side also:

shouldOnlyAcceptStringValues(obj, "a"); // error!
// ------------------------> ~~~
// Argument of type '{ a: number; b: string; c: string; }' is 
// not assignable to parameter of type 'Record<"a", string>'.

shouldOnlyAcceptStringValues(obj, "b"); // okay
shouldOnlyAcceptStringValues(obj, "c"); // okay

The only snag is that the error on the first call is probably not on the argument you expect; it’s complaining about obj and not "a". If that’s okay, great. If not, then you could change the call signature to be the sort of constraint you’re talking about:


type KeysMatching<T, V> = { [K in keyof T]: T[K] extends V ? K : never }[keyof T]
function shouldOnlyAcceptStringValues2<T>(o: T, key: KeysMatching<T, string>): void;
function shouldOnlyAcceptStringValues2<K extends PropertyKey>(
  o: Record<K, string>, key: K
) {
    const okay: string = o[key];
}

The KeysMatching<T, V> type function takes a type T and returns just those keys whose values are assignable to V. And so the call signature will specify T for o and KeysMatching<T, string> for key. Note how I’ve written that call signature as a single overload and the implementation signature is the same as before. If you don’t do that then the compiler is unable to understand that for generic T that T[KeysMatching<T, string>] is assignable to string; it’s a higher-order type inference the compiler can’t make:

function shouldOnlyAcceptStringValuesOops<T>(o: T, key: KeysMatching<T, string>) {
    const oops: string = o[key]; // error!
    // -> ~~~~
    // Type 'T[{ [K in keyof T]: T[K] extends string ? K : never; }[keyof T]]' 
    // is not assignable to type 'string'. 😭
}

See microsoft/TypeScript#30728 for more information.

So in the overloaded version we let the caller see the constraint on key and the implementation see the constraint on obj, which works out better for everyone:

shouldOnlyAcceptStringValues2(obj, "a"); // error!
// ------------------------------> ~~~
// Argument of type '"a"' is not assignable to parameter of type '"b" | "c"'

shouldOnlyAcceptStringValues2(obj, "b"); // okay
shouldOnlyAcceptStringValues2(obj, "c"); // okay

Now the compiler complains about key instead of obj.


Okay, hope that helps; good luck!

Playground link to code

Leave a Comment