Ask
28

Annotation, `as`, or `satisfies` on an object literal: I have three ways to do this and no rule for picking

I keep writing config objects and route maps and I have three ways to type them, all of which compile.

Annotating the variable with the type. Casting the literal with as. Or putting satisfies after it.

They behave differently in ways I only notice later. With the annotation I lose the specific keys and get the general type back when I index into it. With as I sometimes get no error when I typo a property, which frightens me. With satisfies things seem to work but I do not really know what it is doing, so I use it superstitiously.

What is the actual rule? I would like to understand the difference rather than trying all three until the errors go away.

8 answers Share
Report

Answering anonymously, a moderator will review it first.

  • @widening_wren · 3w ago · 3 replies

    All three answer different questions, and once you see which question each one asks the rule is obvious.

    Annotation: const config: Config = {...}. This says "treat this variable as a Config". It checks the literal, and it also replaces what the compiler knows with the declared type. So if Config is Record<string, string>, indexing gives you string and the specific keys are gone. That is not a bug, it is the point: you asked for the general type.

    Cast, const config = {...} as Config. This says "stop checking, I am telling you what this is". It is an assertion, not a check. It will accept a wrong shape as long as it is not wildly unrelated, which is exactly the typo problem you described: and your fear is correct, this is the dangerous one. A cast is a comment claiming something the compiler no longer verifies.

    satisfies, const config = {...} satisfies Config. This says "check this against Config, but keep what you actually wrote". You get the validation of the annotation and the specific literal type. Typos are errors, missing keys are errors, and indexing still gives you the exact keys and the exact value types.

    So the rule: satisfies by default. Annotation when you genuinely want the wider type. as almost never.

    30
    Share
    Reply

    Answering anonymously, a moderator will review it first.

    Report
    • @satisfies_convert · 2w ago · 2 replies

      Rewrote the same config file three ways to see the difference and the practical summary that stuck: annotation checks and forgets the details, satisfies checks and remembers them, and as does not check.

      For a route map you almost always want the second, because you want the keys to stay literal so autocomplete works downstream. That single consequence decides it more often than any principle.

      22
      Share
      Reply

      Answering anonymously, a moderator will review it first.

      Report
      • @widening_wren · 2w ago

        Checks and forgets versus checks and remembers is a better one-liner than mine and it is exactly right.

        13
        Share
        Reply

        Answering anonymously, a moderator will review it first.

        Report
  • @const_assert_ana · 3w ago · 2 replies

    Adding the case where satisfies alone is not enough, because you will hit it immediately with route maps.

    satisfies preserves the literal type as far as normal inference would, and normal inference still widens things. A string property infers as string, not as its exact value, and an array infers as an array rather than a tuple. So you keep your keys but lose the specific values, which is often the thing you actually wanted.

    The pairing is as const satisfies Config:

    • as const says infer everything as narrowly as possible - exact string values, readonly tuples
    • satisfies Config then checks that narrow thing against your type

    Order matters and that is the order.

    This is what you want when you plan to derive types from the object - keyof typeof routes, a union of the exact path strings, a lookup type from key to value. Without as const those come out as string and the whole exercise is pointless.

    One caution: as const makes everything deeply readonly, so if something downstream wants a mutable array you will get an error there instead. That is usually a signal worth listening to rather than a problem to work around, but it does mean you cannot sprinkle it everywhere without thinking.

    Despite the name, as const is not a cast in the dangerous sense. It narrows inference; it does not silence checking.

    25
    Share
    Reply

    Answering anonymously, a moderator will review it first.

    Report
    • @as_const_everywhere · 3w ago

      The combination the second comment is pointing at is worth spelling out: satisfies keeps the specific types of the values, but it does not make anything readonly or stop a nested array widening. For a route map you often want a const assertion as well.

      Two different jobs that people expect one keyword to do.

      18
      Share
      Reply

      Answering anonymously, a moderator will review it first.

      Report
  • @as_const_everywhere · 2w ago

    All three compile, which is why people pick by habit and only find the difference two refactors later.

    7
    Share
    Reply

    Answering anonymously, a moderator will review it first.

    Report
  • @as_is_a_lie · 3w ago · 2 replies

    On as, since "almost never" deserves the exceptions and the reason.

    A cast does not convert anything. It changes what the compiler believes and nothing else, so if you are wrong the error simply moves to runtime, where there is no type system to catch it. Every cast is a claim you are making on your own authority.

    The legitimate uses are narrow and they share a shape - you know something the compiler cannot:

    • After a runtime validation the compiler cannot follow, though a type predicate or a validator that returns a typed result is better and removes the need
    • Narrowing a broad type from an external boundary you have just checked
    • as unknown as T for genuinely untyped interop, which is at least honest about being a two-step lie

    What it should never be is the fix for an error you did not understand. That is the common case and it is how a codebase accumulates assertions that were true when written and are not any more: nothing rechecks them, so they rot silently.

    A practical habit: when you write a cast, write a comment saying why the compiler cannot know this. If you cannot finish the sentence, you have found a bug rather than a typing inconvenience. And if you are casting an object literal specifically, the answer is essentially always satisfies instead.

    1
    Share
    Reply

    Answering anonymously, a moderator will review it first.

    Report
    • @satisfies_convert · 3w ago

      A cast does not convert anything, it changes what the checker is willing to believe. That framing stopped me reaching for it.

      10
      Share
      Reply

      Answering anonymously, a moderator will review it first.

      Report