Reputation: 9285
I want to create a TypeScript function that takes an object and a property within that object, for which the value is a string
. Using <T, K extends keyof T>
works to make sure only keys of T
are allowed as values for the property, but I cannot seem to narrow it down so that the key must also point to a property of type string
. Is this possible?
I tried this:
function getKey<T extends {K: string}, K extends keyof T>(item: T, keyProperty: K): string {
return item[keyProperty];
}
But it just says Type 'T[K]' is not assignable to type 'string'
. Why doesn't the T extends {K: string}
constraint make sure that T[K]
is in fact a string
, or rather, that the supplied K
must fulfill the condition so that T[K]
is a string
?
To be clear, I want to be able to call this function like this:
getKey({foo: 'VALUE', bar: 42}, 'foo') => return 'VALUE';
getKey({foo: 'VALUE', bar: 42}, 'bar') => should not be allowed since 'bar' is not a string property of the supplied object
getKey({foo: 'VALUE', bar: 'ANOTHER VALUE'}, 'bar') => return 'ANOTHER VALUE'
Upvotes: 1
Views: 97
Reputation: 327964
In, {K: string}
, the K
in just the name of the string literal key. It's the same thing as if you had written {"K": string}
:
type Oops = { "K": string };
// type Oops = { K: string; }
Since you want K
to be the type of the key, you need to use a mapped type, which iterates over some union of keys... either {[P in K]: string}
, or the equivalent Record<K, string>
using the Record<K, T>
utility type:
function getKey<T extends { [P in K]: string }, K extends keyof T>(
item: T,
keyProperty: K
): string {
return item[keyProperty]; // no error
}
And your calling code behaves (mostly) as you expect:
getKey({ foo: 'VALUE', bar: 42 }, 'foo'); // okay
getKey({ foo: 'VALUE', bar: 42 }, 'bar'); // error!
// ----------------> ~~~
// number is not assignable to string
getKey({ foo: 'VALUE', bar: 'ANOTHER VALUE' }, 'bar'); // okay
I say "mostly" because it's possible you expect the error in the second line to be on the 'bar'
value passed in for keyProperty
, whereas what actually happens is that the error is on the bar
property of the value passed in for item
.
With a little more finagling you can get that to happen:
type KeysMatching<T, V> = { [K in keyof T]: T[K] extends V ? K : never }[keyof T];
function getKey2<T extends { [P in K]: string }, K extends KeysMatching<T, string>>(
item: T,
keyProperty: K
): string {
return item[keyProperty];
}
Here we constrain K
not just to keyof T
, but to the specific keys of T
whose values are of type string
. We do this with our own KeysMatching<T, V>
utility type. This doesn't change what values will end up being valid, but it does shift where the compiler complains when something is invalid:
getKey2({ foo: 'VALUE', bar: 42 }, 'foo');
getKey2({ foo: 'VALUE', bar: 42 }, 'bar'); // error!
// -----------------------------> ~~~~~
// "bar" is not "foo"
getKey2({ foo: 'VALUE', bar: 'ANOTHER VALUE' }, 'bar');
Okay, hope that helps; good luck!
Upvotes: 2
Reputation: 21
function getKey<T>(item: T, keyProperty: {[K in keyof T]: T[K] extends string ? K : never}[keyof T]): string {
return <string> <unknown> item[keyProperty];
}
Little bit bulky, but meh.. it works. :)
Upvotes: 1