How to keep a literal expression constant (via const assertion), but still type check it against a type to guard against missing/excess properties?
In other words, how prevent the type annotation from overriding the as const assertion, widening the type?
I understand what's happening more or less, and I've already asked around on chat, so I'm pretty sure this doesn't have a solution (in type land), but maybe there's a hack I don't know about.
Use case:
I need to define a config from which I can conditionally infer types based on the values, but at the same time I want to ensure that config contains exactly the same keys as a type it's based off of.
Here's a minimal example. The State type is the blueprint for the keys, and the config object should type check against those keys. But, I also need the config to be a constant so that I can drill down into it and get unit union type, not a widened string type.
playground
type State = Readonly<{
a: number;
b: string;
}>;
const config: Record<keyof State, { value: string }> = {
a: { value: "aaa" },
b: { value: "bbb" }
} as const;
// I want this to be "aaa" | "bbb"
type ConfigValues = (typeof config)[keyof typeof config]["value"];
I can do this:
const config = {
a: { value: "aaa" },
b: { value: "bbb" }
} as const;
// eslint-disable-next-line @typescript-eslint/no-unused-vars
const config_check: Record<keyof State, any> = config;
But the above will only check missing props, not excess props :/.
Note that this is a simple example. In real world the config is more complex and the type I want to infer is conditional based on the values.
Also, I've bumped into this problem pattern several times already, so it doesn't seem like an edge case.
You can use a generic helper function to constraint a type, while keeping it narrow for inference:
function createConfig<P extends string, T extends Record<keyof State, { value: P }>>(
cfg: { [K in keyof T]: K extends keyof State ? T[K] : never }) {
return cfg
}
const config = createConfig({
a: { value: "aaa" },
b: { value: "bbb" },
c: { value: "ccc" } // conditional type error akin to excess property check
})
type keys = (typeof config)[keyof typeof config]["value"]; // "aaa" | "bbb"
type keyA = (typeof config)["a"]["value"] // "aaa"
Playground
If you love us? You can donate to us via Paypal or buy me a coffee so we can maintain and grow! Thank you!
Donate Us With