To achieve the desired behavior, you can modify getProperty()
to be a generic function within K
, a subtype of keyof Baseball
, which will be automatically inferred during function calls. The return type of this function is automatically inferred by the compiler as Baseball[K]
, representing a generic lookup type denoting the K
-keyed property type of a BaseBall
value:
private getProperty<K extends keyof Baseball>(key: string, prop: K) {
return this.data.series[key][prop];
}
It's worth noting that the index signature of SeriesInfo
suggests that it will contain a property of type Baseball
for any key utilized. However, this is improbable, and most keys will return an undefined
property, resulting in seemingly correct code during design time but potential runtime errors:
new BaseballInfo().getName("red sox").toUpperCase(); // TS: okay, JS: error!
This is characteristic of index signatures (refer to microsoft/TypeScript#13778). It's uncertain how you plan to handle this situation. One approach is to have getProperty()
return a value or undefined
if the key is absent. In that case, adjust your types as follows:
type SeriesInfo = {
series: {
[key: string]: Baseball | undefined;
};
};
To acknowledge the possibility of undefined
, you can utilize the optional chaining operator (or equivalent) within getProperty()
:
private getProperty<K extends keyof Baseball>(key: string, prop: K) {
return this.data.series[key]?.[prop]; // utilize the ?. operator
}
With this setup, getProperty()
now returns Baseball[K] | undefined
, and getName()
returns
string | undefined</code, resulting in this code now being flagged as an error:</p>
<pre><code>new BaseballInfo().getName("red sox").toUpperCase(); // error!
// ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ <-- potentially undefined
Alternatively, you can specify the type of series
by allowing the compiler to infer the keys and restricting only those keys to be input, by adding an additional generic parameter:
type Series = BaseballInfo['data']['series'];
class BaseballInfo {
private data = {
'series': {
mets: { name: 'Mets', lng: 12.34, lat: 12.34 },
yankees: { name: 'Yankees', lng: 12.34, lat: 12.34 },
}
};
constructor() { }
public getName<S extends keyof Series>(key: S) {
return this.getProperty(key, 'name');
}
private getProperty<S extends keyof Series, B extends keyof Baseball>(key: S, prop: B) {
return this.data.series[key][prop];
}
}
As a result, the error message changes to:
new BaseballInfo().getName("red sox").toUpperCase(); // error!
// ----------------------> ~~~~~~~~~
// "red sox" is not assignable to "yankees" | "mets"
Hopefully, this resolves your issue. Best of luck!
Playground link for the code