openlang

Annotation or satisfies for a config object

For config objects I've mostly moved from const config: Config = {...} to {...} satisfies Config. With the annotation, config.mode reads back as 'light' | 'dark' even though I wrote 'dark'. With satisfies, the object is checked against Config and the variable keeps the literal types it was written with, so config.mode is 'dark', and comparing it to 'light' later is a type error instead of a dead branch.

Both forms still flag a misspelled property on the literal. The 4.9 notes show it with a bleu key checked against Record<Colors, string | RGB>.

Where it fooled me was a routes table written as satisfies Record<string, Route>. A misspelled route name isn't an excess property there. Every string key is allowed, so the check passes and the typo turns into a route nobody links to. The annotation has the same hole. satisfies just made me feel more checked than I was.

For a fixed set of keys I now spell the union out, Record<RouteName, Route>, and let the object fail when it grows a stranger.

后续讨论 6

顺着彼此的依据读下去
  1. 固定链接

    One more difference, on optional properties. With the annotation, a timeout?: number you left out still reads as number | undefined, and every reader has to decide what undefined means. With satisfies, the property isn't in the inferred type at all, so config.timeout fails with TS2339, property does not exist.

    That is a sharper answer and a different one. Add timeout to the literal later and every reader that was rejected starts compiling. Pick by whether callers should handle a missing value or never see one.

  2. 固定链接

    For anything exported I'd go the other way. A satisfies object exports its literal type too, so every module importing config now depends on the values inside it. Change 'dark' to 'light' and a comparison three files away stops compiling. That code was relying on a value, not on a type, and nobody decided it should. For application config I still prefer the annotation. Config says what the code may rely on, nothing more.

  3. 固定链接

    The root makes this sound simpler than it is. satisfies doesn't keep every literal you write.

    type Config = { mode: 'light' | 'dark'; port: number };
    
    const config = { mode: 'dark', port: 8080 } satisfies Config;
    
    const m: 'dark' = config.mode;  // ok
    const p: 8080 = config.port;    // error, port is number

    port widens to number because Config gives it number as the contextual type. mode stays 'dark' only because Config declares a union of string literals. So it keeps the more specific inferred type, and how specific depends on Config. The worry about importers leaning on values applies to mode and mostly not to port.

  4. 固定链接

    So the user opens settings, picks the dark theme, and that choice gets written to a JSON file. Next launch the app reads that file back, and satisfies was never anywhere near it (it only ever looked at the literal in the source). The literal config says 'dark', the saved file might say 'Dark' or nothing at all. Do people keep the satisfies object as the defaults and parse the saved file into Config with a runtime check, or is there a way to keep the two from drifting that I'm missing?

  5. 固定链接

    Answering my own question after trying it. The satisfies literal became defaults, the saved file goes through a small parseSettings(raw: unknown): Partial<Config>, and the app runs on const config: Config = { ...defaults, ...saved }. The annotation on the merged value also takes care of selim70's worry, since importers only see Config.

    The part that bit me was the parser. My first version copied every known key and set it to undefined when the value was junk. Spreading an explicit undefined wipes the default, and the types didn't object (no exactOptionalPropertyTypes in that project). So a user who typed "Dark" by hand got neither dark nor light, just a theme picker with nothing selected. Now the parser leaves a bad key out entirely, and the settings screen shows a small note that one saved value was ignored.

  6. 固定链接

    Can confirm the TS2339 part, that's how it showed up for me. A tooltip helper read defaults.timeout straight off the satisfies object and failed to compile, because the literal never had a timeout. Pointing it at the merged config gave number | undefined, which is the honest type there anyway, since a saved file can leave it out too.

同一个问题的其他角度

  1. Does an empty palette need its own case?

    An empty color list means two different things in a swatch editor, and Save should be able to tell them apart. Color validity…

    回复 8最近活动 fsharphaskell
  2. Is a read-only List<Int> a snapshot?

    List<Int> is a read-only view, not a snapshot. It tells a consumer which operations it can call through that reference. It pr…

    回复 4最近活动 kotlin
  3. Validating JSON before the discriminated union

    A type assertion only tells the compiler what to believe. An external response starts as bytes and says nothing about its var…

    回复 9最近活动 typescript