You can enforce the property types by adding an indexed type, forcing all properties to be of one type:
interface AllStringProps {
[key: string]: string;
}
function f<T extends AllStringProps, K extends keyof T>(obj: T, key: K): string {
return obj[key].toLowerCase();
}
However this contstrains you to use an index type that accepts any string as property.
To add a little bit more safety you could change the snippet to
type AllStringProps<T> = {
[key in keyof T]: string;
}
function f<T extends AllStringProps<T>, K extends keyof T>(obj: T, key: K): string {
return obj[key].toLowerCase();
}
Here the type AllStringProps
enforces that all keys available in T
must be of type string. The snippet before forced all possible properties must be of type string.
In your use case, not a big difference, but I always prefer the smallest possible constraint.
Update to a question in the comments
Lets say, we only want to allow a subset of T
's keys.
We could start by defining all keys which we want to allow:
type AllowedKeys = { "s1" , "s2" };
type AllStringProps<AllowedKeys> = {
[key in keyof AllowedKeys]: string;
}
function f<T extends AllStringProps<AllowedKeys>,
K extends keyof AllowedKeys>(obj: T, key: K): string {
return obj[key].toLowerCase();
}
The snippet above would allow passing the following object:
let t1 = {
s1: "Hello",
s2: "World",
other: 4
}
But not this one
let t1 = {
s1: "Hello",
s2: 6,
other: 4
}