You signed in with another tab or window. Reload to refresh your session.You signed out in another tab or window. Reload to refresh your session.You switched accounts on another tab or window. Reload to refresh your session.Dismiss alert
{{ message }}
Repository navigation
Inconsistent literal type widening with satisfies operatorΒ #51516
While using a constant string inside of an array or object, it's considered a const but while using it otherwhere, you should cast it as a const if you mean it.
Hamed Aghabalazadeh (@hameda169) Why is it considered a const inside an array or object ? It isn't usually so. Normal inference without satisfies or as would have it as string, even when assigned to a const.
One could argue that since satisfies has no hint of immutability in it and since the tuple and object types are mutable, the correct types should be ["A" | "B"] and { p: "A" | "B" }
I'm a bit surprised it's inferring literal types at all. satisfies isn't supposed to change the inferred type of an expression and without it you get string[] and { p: string } for x and o respectively.
We have a bit of a competing invariant problem here: The design goal of the satisfies operator is that
The overall expression type is the type of the asserted expression
The expression should be contextually typed by the asserted type
These principles are in direct conflict when it comes to literal types and there's not a clear way to resolve the situation without just creating a different inconsistency. The root cause is that the process that causes the bare literal "A" to decay to string (widening) is different from the process that causes e.g. the literal [1, 2] to become [number, number].
Maybe some day someone smarter than all of us can unify contextual typing and widening into a single process and fix all this (this is not sarcasm btw, I believe it's possible), but until then all we can really say is that these types are fully consistent with the different processes which produce them and that we'd actually be introducing more inconsistency by trying to undo those processes.
For something like
lett=[1,2]satisfies[]|any[]// [number, number]
I think you could make a variety of arguments about what the "correct" type of t depending on the names of variables, what line of code follows, etc.. It's just not clear. The observed type of contextually-typed expressions was previously very hard to get at in TypeScript but this feature makes it fairly transparent, unfortunately, since people's mental models of how contextual type works are frequently wrong (and understandably so; they are subtle).
FWIW my mental model of contextual typing has always been βtype inference, but contravariantβ - in other words that you infer something about the source of an assignment from its target rather than (as TypeScript usually does) vice versa, and the rules for these two styles of inference are disjoint. Full unification a la Hindley-Milner would likely make this concept go away entirely, since itβs inherently a bidirectional inference process.
under this model though, satisfies is kind of an odd duck since the RHS of satisfies is explicitly not the assignment target by design.
The new satisfies operator lets us validate that the type of an expression matches some type, without changing the resulting type of that expression.
If that is not entirely correct it feels like the documentation around the operator should elude to there being situations where the inference is impacted, maybe with links to read more for the curious reader. That said I do appreciate the need for a certain level of brevity and explaining the intuition to have for these cases is no easy task.
Reacted by Titian Cernicova-Dragomir, Bruce Pascoe and Matt Kantor
Neat! But now let's say we change the color field in Config to color?: "primary" | "secondary", the error for satisfies will be very confusing because it will no longer highlight that defaults.color has the wrong value inline, it will instead give a very long error about the resulting type not satisfying the interface:
Is there a way to get the best of both worlds? (Caveat: our real types have a lot more fields and would most likely see the type changing for multiple fields at the same time, so even if the error in the screenshot is still parseable, it'd be pretty cryptic in a real-world example due to the number of fields not to mention generics/inheritance, etc)
I'm actually happy to have a way to force the inference of mutable tuples. I will be recommending this when the need arises:
lett=[1,2]satisfies[]|any[]// [number, number]
I was just making sure this is intentional behavior and not a bug. Like Ashley Claymore (@acutmore) mentioned, the high level description of the satisfied operator confused me into thinking this behavior might be bug:
The new satisfies operator lets us validate that the type of an expression matches some type, without changing the resulting type of that expression.
The new satisfies operator lets us validate that the type of an expression (evaluated with a contextual type of the asserted type) matches some type, without coercing the resulting type of that expression to the provided type.
If the operator didn't work this way, then [1, 2] satisfies [number, number] would actually fail, which I think would seem pretty weird.
If the operator didn't work this way, then [1, 2] satisfies [number, number] would actually fail, which I think would seem pretty weird.
Agreed, with the caveat that this is then squarely in "design limitation" territory - it feels very much like the intent of satisfies is that such an assertion should succeed without changing the type of the expression, and the only reason this is impossible today is because of internal compiler implementation details.
it feels very much like the intent of satisfies is that such an assertion should succeed without changing the type of the expression,
I guess the question is how you would interpret x => (x + 1) satisfies (n: number) => number. For [1, 2] we can reasonably discuss its narrowest possible type readonly [1, 2], but for a function expression it's sort of meaningless to talk about its narrowest possible type (in the absence of a completely different type inference algorithm).
In this playground, we have two identical objects. But with the second object, we add a satisfies constraint, which causes it to throw a compiler error. But the compiler error has nothing to do with the constraint itself (which should always pass) - it has to do with the satisfies operator actually mutating the type of the object itself (which causes the as const part to not work properly, I guess).
I think that this is just the same widening behavior that the OP is describing, but I'm not 100% sure. (I can open a new issue if needed.)
Like others in the thread, I would naively expect the satisfies operator to be a "verify my thing" operation, not a "subtly modify the thing I just created while also verifying it" operation, so some more documentation surrounding this would be appreciated. (Right now, I can't even find any documentation for satisfies on the official handbook!)
Activity
hameda169 commentedon Nov 14, 2022
While using a constant string inside of an array or object, it's considered a
constbut while using it otherwhere, you should cast it as a const if you mean it.dragomirtitian commentedon Nov 14, 2022
Hamed Aghabalazadeh (@hameda169) Why is it considered a
constinside an array or object ? It isn't usually so. Normal inference withoutsatisfiesoraswould have it asstring, even when assigned to aconst.One could argue that since
satisfieshas no hint of immutability in it and since the tuple and object types are mutable, the correct types should be["A" | "B"]and{ p: "A" | "B" }fatcerberus commentedon Nov 14, 2022
I'm a bit surprised it's inferring literal types at all.
satisfiesisn't supposed to change the inferred type of an expression and without it you getstring[]and{ p: string }forxandorespectively.dragomirtitian commentedon Nov 14, 2022
Bruce Pascoe (@fatcerberus) That was my understanding of the operator as well. The behavior is surprising.
It also changes the type of an array to a tuple if we use the trick of constraining to
[] | any[]. This is how I originally stumbled on this:Playground Link
RyanCavanaugh commentedon Nov 14, 2022
We have a bit of a competing invariant problem here: The design goal of the
satisfiesoperator is thatThese principles are in direct conflict when it comes to literal types and there's not a clear way to resolve the situation without just creating a different inconsistency. The root cause is that the process that causes the bare literal
"A"to decay tostring(widening) is different from the process that causes e.g. the literal[1, 2]to become[number, number].Maybe some day someone smarter than all of us can unify contextual typing and widening into a single process and fix all this (this is not sarcasm btw, I believe it's possible), but until then all we can really say is that these types are fully consistent with the different processes which produce them and that we'd actually be introducing more inconsistency by trying to undo those processes.
For something like
I think you could make a variety of arguments about what the "correct" type of
tdepending on the names of variables, what line of code follows, etc.. It's just not clear. The observed type of contextually-typed expressions was previously very hard to get at in TypeScript but this feature makes it fairly transparent, unfortunately, since people's mental models of how contextual type works are frequently wrong (and understandably so; they are subtle).fatcerberus commentedon Nov 15, 2022
FWIW my mental model of contextual typing has always been βtype inference, but contravariantβ - in other words that you infer something about the source of an assignment from its target rather than (as TypeScript usually does) vice versa, and the rules for these two styles of inference are disjoint. Full unification a la Hindley-Milner would likely make this concept go away entirely, since itβs inherently a bidirectional inference process.
under this model though,
satisfiesis kind of an odd duck since the RHS ofsatisfiesis explicitly not the assignment target by design.acutmore commentedon Nov 16, 2022
The 4.9 announcement post says:
If that is not entirely correct it feels like the documentation around the operator should elude to there being situations where the inference is impacted, maybe with links to read more for the curious reader. That said I do appreciate the need for a certain level of brevity and explaining the intuition to have for these cases is no easy task.
blixt commentedon Nov 16, 2022
One problem I've noticed applying
satisfiesto our code base is that it's hard to control type widening while keeping the benefits ofsatisfies:Pre-4.9:
4.9:
Better! But, it would be nice if we could see the current defaults in the type of
defaults, so I do this:Neat! But now let's say we change the color field in
Configtocolor?: "primary" | "secondary", the error forsatisfieswill be very confusing because it will no longer highlight thatdefaults.colorhas the wrong value inline, it will instead give a very long error about the resulting type not satisfying the interface:Is there a way to get the best of both worlds? (Caveat: our real types have a lot more fields and would most likely see the type changing for multiple fields at the same time, so even if the error in the screenshot is still parseable, it'd be pretty cryptic in a real-world example due to the number of fields not to mention generics/inheritance, etc)
dragomirtitian commentedon Nov 16, 2022
I'm actually happy to have a way to force the inference of mutable tuples. I will be recommending this when the need arises:
I was just making sure this is intentional behavior and not a bug. Like Ashley Claymore (@acutmore) mentioned, the high level description of the
satisfiedoperator confused me into thinking this behavior might be bug:RyanCavanaugh commentedon Nov 16, 2022
I guess the precise wording would be that
If the operator didn't work this way, then
[1, 2] satisfies [number, number]would actually fail, which I think would seem pretty weird.acutmore commentedon Nov 16, 2022
I like that wording, nicely put.
fatcerberus commentedon Nov 17, 2022
Agreed, with the caveat that this is then squarely in "design limitation" territory - it feels very much like the intent of
satisfiesis that such an assertion should succeed without changing the type of the expression, and the only reason this is impossible today is because of internal compiler implementation details.RyanCavanaugh commentedon Nov 17, 2022
I guess the question is how you would interpret
x => (x + 1) satisfies (n: number) => number. For[1, 2]we can reasonably discuss its narrowest possible typereadonly [1, 2], but for a function expression it's sort of meaningless to talk about its narrowest possible type (in the absence of a completely different type inference algorithm).Zamiell commentedon Nov 18, 2022
Here's a slightly different example of
satisfieschanging the type of an object:https://www.typescriptlang.org/play?ts=5.0.0-dev.20221118#code/KYDwDg9gTgLgBDAnmYcASBDAzgQQDZ4CiAdgK4C2A0sIlgDwAqcoMwxAJlnFjFAJbEA5nAA+cMuQBGwKAD44AXjgBvAFBw4UYBnYRieRHADaAaxpwBcBgF0AXHFLETxCAHdiAblUBfL6rYUcACyiCSBahoaAGoYeKTAAIwANOqRMXHAAEwpkXDp8QDMKd6qqgD0AFQVcOrVOHDkGGBgAsIQAGbBoRIIEHAY4hTSUAikYHjAAHS1NXDVDAAWfFwL2OJ9MlDQWNNzZf7g0PAAxno8wQCaAPqEAHIAqkFXDADyVw9BAEKEAErP9wAFAAyhCuCXsEU02l0+kMpnMlhCYXIdjgP2hegMQSadAkwySxhA9jxMgJiGJQxk1lkXm8ihUqSMSIkk3yiVRxGArmCTQAFEZUhojAAGAlGAC0yTgBWs1hycGsAEoUozmRRWbF4pkOVyeWB+YLjKLjJKCTK5YajFKrUVpbL5UqVUK1eQNRkZcTddj9QLciKxQA2AmS+2W61BuAAdlDfuyxgAHAToxaNI6fP0uKdiDw-JUKjN6o1mq04B0usjev1BlIZKNxlM4LzFqhQBhjvAsBhyKgYEshBm4NJ2tAporajNFss4KsuJttoPgMcMKQsKhXKgdOxgOwqwAiTswZbtPjALC7uBZnhQDACGCTOAAdVW8EEfAAbqeAPy1fagSCwC8zngIJrjuR5njeD5vj+BhARBK5MghVItB0TE4TMQxEW6ChUXRVDYW9XFKSgMUiWrfE4HJciqRpdMlEhJlsNdNkEh1blvQNP1jQlKVzQdZVSmdJi3S1Ni9U4yJ-RNXiY0k60EltPjUjTVVhLZD1xC9PlfUk7iIxDFM5MDJNZKFOMjETKNZLTOk1kvDsMEPLBj1PdBsHwIgJGoWg6BdWigA
In this playground, we have two identical objects. But with the second object, we add a
satisfiesconstraint, which causes it to throw a compiler error. But the compiler error has nothing to do with the constraint itself (which should always pass) - it has to do with thesatisfiesoperator actually mutating the type of the object itself (which causes theas constpart to not work properly, I guess).I think that this is just the same widening behavior that the OP is describing, but I'm not 100% sure. (I can open a new issue if needed.)
Like others in the thread, I would naively expect the
satisfiesoperator to be a "verify my thing" operation, not a "subtly modify the thing I just created while also verifying it" operation, so some more documentation surrounding this would be appreciated. (Right now, I can't even find any documentation forsatisfieson the official handbook!)