I am attempting to create a generic settings utility that provides some basic return types. Our application stores all settings in a centralized dictionary and instead of manually specifying types for each key, I'd like to provide a map of possible types.
As an example, say we have the map of settings keys:
const UserSettings = {
DEFAULT_ORGANIZATION: 'user_profile.default_organization',
THEME: 'user_profile.theme'
} as const;
I would like to create a corresponding map which looks like:
type UserSettingsTypes = {
'user_profile.default_organization': boolean;
'user_profile.theme': { primary: string; secondary: string };
};
So that I can write a settings look-up function that looks like:
const organization = readSetting<UserSettingsTypes>(UserSettings.DEFAULT_ORGANIZATION);
Where organization
has the correct type of boolean
.
I've tried adding a generic to readSetting
that looks like:
const readSetting = <T extends Record<string, unknown>, K = keyof T>(key: K): T[K] => {
// ...
};
But it complains that "Type 'K' cannot be used to index type 'T'"
Is it possible to provide a mapped return type like this?
If so, how should I structure K
so that it can correctly return the mapped type in T
?
Here is an example showing the issue: https://tsplay.dev/m0d2qm
Conceptually you want something like
declare const readSetting:
<T extends object, K extends keyof T>(key: K) => T[K]
where K
is constrained to keyof T
. Unfortunately you can't call a generic function like readSetting<UserSettingsTypes>(UserSettings.DEFAULT_ORGANIZATION)
where you manually specify T
but have the compiler infer K
for you. TypeScript doesn't support partial type argument inference as requested in microsoft/TypeScript#26242. You either have to let the compiler infer both T
and K
(which is impossible without an argument of type T
), or you have to manually specify both T
and K
(which is redundant for K
). No, using a type argument default like K = keyof T
doesn't help; you'd just get the default instead of inference.
Until and unless microsoft/TypeScript#26242 is implemented, you need to work around it somehow. The most common workaround I use here is currying, where you split the single generic function into multiple. In your case that looks like:
declare const readSetting:
<T extends object>() => <K extends keyof T>(key: K) => T[K]
Now you can call readSetting<UserSettingsTypes>()
, manually specifying T
. That returns a new generic function of type <K extends keyof UserSettingsTypes>(key: K) => UserSettingsTypes[K]
, which you can call with a function argument and K
will be inferred for you:
const organization =
readSetting<UserSettingsTypes>()(UserSettings.DEFAULT_ORGANIZATION);
// const organization: boolean
So, while there's that weird function call ()
in the middle there, it's very close to what you wanted originally and it gives you the return type you expect.