The main issue lies in your explicit annotation of the config
variable as type ParameterDictionary
, like this:
const config: ParameterDictionary = {
foo: { default: "test" },
bar: { default: 1 },
baz: { default: true },
}
This approach negates more specific information that the compiler could infer from the object literal initializer. It's better to let the compiler infer the type instead:
const config = {
foo: { default: "test" },
bar: { default: 1 },
baz: { default: true },
};
If you still want a warning if config
doesn't match the expected type, consider using the satisfies
operator on the initializer:
const config = {
foo: { default: "test" },
bar: { default: 1 },
baz: { default: true },
} satisfies ParameterDictionary;
By implementing this, the compiler will understand that config.foo.default
is a string
, etc.
Next, adjust the call signature of get()
to allow key
to be any property key, not limited to known keys of config
. This modification might look something like:
function get<T extends ParameterDictionary, K extends PropertyKey>(
config: T, key: K
): K extends keyof T ? T[K]["default"] : undefined;
function get(config: ParameterDictionary, key: string) {
return config[key]?.default; // Fix implemented here
}
The return type of get()
is conditional based on whether K
(type of key
) is a known key of T
(type of
config</code). If yes, it gets the type of <code>default
property of
T[K]
; otherwise returns
undefined
.
const a = get(config, "foo"); // const a: string
const b = get(config, "bar"); // const b: number
const c = get(config, "baz"); // const c: boolean
const d = get(config, "boz"); // const d: undefined
(Note: Fix was applied to ensure last result is truly undefined
at runtime and not trigger a TypeError
)
It's important to note that TypeScript object types are not strictly "sealed"; a type lacking mention of a key doesn't guarantee absence of such key on instances. Returning undefined
for unknown keys may not always be precise:
const config2 = {
foo: { default: "test" },
bar: { default: 1 },
baz: { default: true },
oops: { default: "oops" }
} satisfies ParameterDictionary; // valid
const oops: typeof config = config2; // also valid
const e = get(oops, "oops");
// result: undefined but should be "oops"
console.log(e);
This situation relies on aliasing and widening, posing an unlikely scenario, though worth noting. Implementations can opt for returning unknown
or reject unknown keys altogether for safer operation.
Playground link to code