TypeScript: Satisfies does not work with const assertion
Bug Report
š Search Terms
satisfies, as const, literal
š Version & Regression Information
- This is a crash
- I was unable to test this on prior versions because
satisfiesis new in 4.9.0 beta
⯠Playground Link
š» Code
type Colors = "red" | "green" | "blue";
type RGB = readonly [red: number, green: number, blue: number];
type Palette = Readonly<Record<Colors, string| RGB>>;
const palette1 = {
red: [255, 0, 0],
green: "#00ff00",
blue: [0, 0, 255]
// Expect to pass but throws error
} satisfies Palette as const;
š Actual behavior
Fails with error:
āconstā assertions can only be applied to references to enum members, or string, number, boolean, array, or object literals.
š Expected behavior
No error because it is a literal and satisfies should not change the type. Also, other type assertions work fine, so const should as well.
About this issue
- Original URL
- State: closed
- Created 2 years ago
- Reactions: 28
- Comments: 23 (5 by maintainers)
just wrap the the literal with const assertion in parenthesis before adding satisfies
Just sharing an alternative based on type-fest ReadonlyDeep.
That should work with any type (map, arrays, nestingā¦).
I donāt think thatās the ask here, to be fair. It seems like the intent is to do the
satisfiescheck first, and only then apply the transformations inherent toas const. Which is not currently possible.Yes, it works when reversed as I show in the playground link, but that just creates an unnecessary restriction to make sure everything in the
satisfiestype isreadonlybecause the check will fail otherwise.The compiler may be treating it as an expression, but itās not, and therein lies the bug.
Try reversing the order of
satisfiesandas const. Having thesatisfiesclause first makes the compiler think youāre trying to applyas constto an expression instead of a bare literal, so itās rejected for the same reason as(1 + 1) as constwould be.This is immaterial as
as constcanāt be applied to arbitrary expressions like other type assertions can.As a workaround the Immutable<T> type of @lauf/store aligns with the recursive readonly behaviour of
as constin case you absolutely have to put up with the order in 4.9.I typically end up with Immutable in my codebases for reasons like this, even where Iām not using the store itself - itās a tiny package anyway, and getting smaller in v2.0.0. Regardless it wonāt be bundled if imported as type-only.
Hereās an example where I can derive
CompanyIdfrom the contents of an as const array, which is nevertheless known to be an array ofCompanyitems, giving me auto-completion.Iāve found this to be useful enough to need it in all my projects anyway, so maybe a Const<T> is a candidate for a core Typescript utility type so that people can easily align with
as const?trackEvents.ts
The type
TrackEventsshould be:It wouldnātāwhich is the problem here.
as constapplies to object/array literals, so even ifsatisfies T as constworked now, it wouldnāt do what you wantāit would just infer the readonly types to begin with and subsequently fail the check. In order to do what you want the narrowing implied by a const assertion would need to be done as a separate step, which requires an architectural change to the compiler.(in simpler terms
{ ⦠} as constis one atomic expression potentially with a different type from{ ⦠}and breaking it apart, e.g. by putting asatisfiesclause in the middle, makes no sense with the current architecture)That works because the const assertion applies to the array literal
[ ... ], not thesatisfiesexpressions directly.What Ryan is saying is that the behavior you want isnāt possible with the current way const assertions work because
as constisnāt a type transformation the way other type assertions are, i.e. it doesnāt take something typed asnumber[]and give back areadonly [ 1, 2 ], it directly affects type inference of the literal itās applied to, namely by inferring narrower types to start with. So a naive implementation of this would still have the same problem asas const satisfies Tdoes because it would be impossible to do the type check ābeforeā the const assertion is applied (thereās no ābeforeā).Giving it some more thought, in the OP we can just reverse the order of operations, and it works, but arguably that actually doesnāt generalize:
Iām not sure how to resolve this situation coherently. The implied algorithm seems to be:
{ m: [1, 2] }and see if itās assignable to of{ m: number[] }{ m: number[] }{ m: [1, 2] } as constand see if itās assignable to{ m: readonly [1, 2] }But in TypeScript, expressions only have one type. If we just āthreaded throughā the
constcontext into the expression, this would happen:{ m: [1, 2] }in a const context and see if itās assignable to of{ m: number[] }Another way to put it is that
as constisnāt a type-to-type transform, but rather itās a modifier that contextually changes the interpretation of literals, which is why it only makes sense to apply directly to expressions of a literal form.The same thing is broadly true of contextual types in general, which again arenāt a type-to-type transform, but rather have effects on the interpretation of literals and other expressions. In the original
satisfiesdiscussion we talked about whether the outer contextual type should matter ā I initially argued in favor of this because usually more contextual typing just only accepts more correct programs, but there were circularities and other problems introduced by this that ultimately made us decide against doing so. And already Iāve found some interesting cases where an unexpected benefit ofsatisfiesis that you can usesatisfies unknownto remove any outer contextual type in the rare cases where itās negatively affecting something.as constseems similar in spirit here and itād be weird to thread-in theconstness while not also threading-in the contextual type from anas T.https://github.com/microsoft/TypeScript/pull/55229 addressed this. I think that this addressed most of the concerns here - you still have to
as const satisfies T(in this order) but thatās not a defect and has been initially classified as working as intended. Given the mentioned PR, this can probably get closed. cc @RyanCavanaugh@steverep The confusion here is essentially linguistic: think āprairie dogā vs āprairie manā. Syntactically, the const assertion looks like the former to the compiler.
As I understood from your writing in #47920,
e satisfies Tshould returntypeof e, which implies to me (and probably most users?) that thesatisfiesoperator should not affect any further operations one. So at the very least the error returned seems incorrect and confusing.Well, that depends on
fandg, but if I go ahead and follow that through:In other words, if the
satisfiesoperator is widening the literals to their primitive types, then sure they are not equivalent. But if thesatisfiesoperator is doing nothing more than a check onx, then we should viewg(x) = x, and they are absolutely equivalent so long asTis adjusted accordingly.And stating that thereās only one order that works is not correct either. The following works just fine:
I need much more time to digest your latter comments as I am not an expert on the compiler code, but thanks for starting to pull a 180. š
It absolutely is an expression, though.
Regardless,
f(g(x)) === g(f(x))isnāt an identity. Itās completely correct to say that you must writeas const satisfies T, since thatās the only order of applying those operations that works.