Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

Prevent type annotation from type widening a const expression

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.

like image 800
dwelle Avatar asked Oct 21 '25 18:10

dwelle


1 Answers

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

like image 107
bela53 Avatar answered Oct 24 '25 08:10

bela53



Donate For Us

If you love us? You can donate to us via Paypal or buy me a coffee so we can maintain and grow! Thank you!