I want to design a function in TypeScript which access a field name of a complex structure and returns that field's type.
For example, there's a Settings
type with several fields:
export interface Settings {
apiConfig?: ApiConfig;
presentation?: PresentationSettings;
}
I want to have a function getSettings
which I could use as:
let cfg: ApiConfig = getSettings('apiConfig');
let ui: PresentationSettings = getSettings('presentation');
My first approach was usage of keyof:
function getSettings(name: keyof Settings): Settings[keyof Settings] {
if (name == 'apiConfig') {
return store.get(name) as ApiConfig;
}
if (name == 'presentation') {
return store.get(name) as PresentationSettings;
}
}
But it means that return type is basically a union of ApiConfig | PresentationSettings. So I'll have to cast:
const ui = <PresentationSettings>getSettings('presentation');
Non-ideal.
Next approach was generics:
export function getSettings<T extends Settings[keyof Settings]>(
name: keyof Settings
it can be used as:
const ui = getSettings<PresentationSettings>('presentation');
But while implementing the function:
export function getSettings<T extends Settings[keyof Settings]>(
name: keyof Settings
{
if (name == 'apiConfig') {
return store.get(name) as ApiConfig;
}
}
I'm getting error: Type 'ApiConfig' is not assignable to type 'T'. 'ApiConfig' is assignable to the constraint of type 'T', but 'T' could be instantiated with a different subtype of constraint 'ApiConfig | PresentationSettings'.ts(2322)
So it has to be cast to any
.
Non-ideal as well.
Is there any other ways?
Particularly I'd like to call it w/o any cast:
const ui = getSettings('presentation');
and have an instance of PresentationSettings
.
Is that possible?
CodePudding user response:
The function definitely needs to be generic. But T
should not be the return type but rather just the key. The return type can use T
to index Settings
.
function getSettings<T extends keyof Settings>(name: T): Settings[T] {
return {
apiConfig: store.get(name) as ApiConfig,
presentation: store.get(name) as PresentationSettings
}[name]
}
If you want no errors in the implementation, you will have to use some kind of map structure like the one shown above.
The problem with this implementation is that each store.get
call in the object will be eagerly executed which is not ideal if it is an expensive call. To avoid this you may just use a normal switch
statement. But you are gonna need some assertions to silence the errors.
function getSettings<T extends keyof Settings>(name: T): Settings[T] {
switch (name) {
case "apiConfig": { return store.get(name) as Settings[T] }
case "presentation": { return store.get(name) as Settings[T] }
default: { return undefined as Settings[T] }
}
}
If you want to be fully type-safe and have no eager execution of all paths you can use a map containing functions. The only downside here is the additional function call and that it might seem too verbose.
function getSettings<T extends keyof Settings>(name: T): Settings[T] {
const mapping: {
[K in keyof Settings]: () => Settings[K]
} = {
apiConfig: () => store.get(name) as ApiConfig,
presentation: () => store.get(name) as PresentationSettings
}
return mapping[name]()
}