Given an object type (or class type), I want to write a function that accepts the object and a list of its keys. However, I only want to allow keys that map to a value of a
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'.