JHH
JHH

Reputation: 9285

Typescript function where object has a property of type string determined by another parameter

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

Answers (2)

jcalz
jcalz

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!

Playground link to code

Upvotes: 2

Martin Bedn&#225;ř
Martin Bedn&#225;ř

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

Related Questions