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
satisfies operator changes the type of an expression #55189
This is the behavior in every version I tried, and I reviewed the FAQ for any entries related to declarations, expressions, narrowing, etc. and couldn't find anything
typeFoo={a: boolean;}constf={a: true};f.a=false;// no problem!constg={a: true}satisfiesFoo;g.a=false;// uh oh//^ Type 'false' is not assignable to type 'true'.
🙁 Actual behavior
The type of g is { a: true; } when satisfies Foo is used, and { a: boolean; } if it's omitted.
🙂 Expected behavior
The type of g is { a: boolean; } regardless of whether satisfies Foo is used.
This is very specific to boolean. The "problem" with boolean is that it's actually a union (true | false). So what you see here, I think, is roughly equivalent to the same situation with a string like here:
IIRC, there are some places in the compiler that special case boolean for this reason so maybe this issue can be classified as a bug / possible improvement but I'm not sure.
The "problem" with boolean is that it's actually a union (true | false). So what you see here, I think, is roughly equivalent to the same situation with a string like here:
This is working as intended. The satisfies operator provides a contextual type for the left hand expression, and contextual types can (and are intended to) influence their target expression. In this particular case, the boolean contextual type for the expression true causes the compiler to preserve the literal type true (instead of widening it to boolean).
I suppose the issue here is that the release notes should have stated this.
EDIT: oh I see this has already been said here a minute or two ago
As I said in another issue the wording of the description of satisfies in the release notes is a bit unfortunate. It most definitely can "change" the type of the expression, via providing a contextual type. I think they mostly meant it won't obliterate the old type in favor of the new type, like an annotation or assertion would.
Aside from the wording in the documentation making me think that the type of the variable wouldn't be affected by its initial value expression having satisfies after it, the thing I was struggling with was why contextual typing was producing a literal type rather than the full union type I'd have expected from either Foo or from the normal inference you'd get without satisfies.
After re-reading the original proposal thread for satisfies (and the feedback reset one) I was still confused, but time I noticed some stuff that prompted me to search for discussions about contextual typing unions. I stumbled on the following extremely relevant part of a comment:
when a primitive literal is contextually typed by a union containing that literal's corresponding type, then it retains its actual literal type instead of widening to its parent primitive
I had no idea that was the case. As a side note, I was originally confused about why Mateusz Burzyński (@Andarist) was bringing up the union nature of booleans, but now it seems they were probably referring to this behavior. I've looked around a bit more but haven't been able to find a clear explanation of why contextually typing unions behaves this way. I've been reading and rereading stuff for a while though so my eyes may have glazed over a bit.
If someone can provide more info about this or point me at some documentation1 or relevant discussion about this that I've missed I'd appreciate it. I suppose it's like a lot of things in the type system where it's something that makes possible the things that I do daily but that I've never noticed it because in other cases it's what I want. I found it totally surprising and unintuitive in this case though.
I'm satisfied that this behavior isn't a bug now, so I'm closing the issue. It would be nice if folks would consider a documentation update to make it clearer that because satisfies provides contextual typing its presence in a declaration can affect the type of a variable.
1. I didn't notice anything in the existing handbook page on type inference & contextual typing which covered this.
typeFooey={foo: "foo"|"bar"};consterr={foo: "qux"}satisfiesFooey;// error, goodconstok={foo: "bar"}satisfiesFooey;// no error, also good
Normally, properties in an object literal that are initialized with a primitive literal are widened when the type of the object is inferred; in this case, ok.foo normally widens to string which would then fail the satisfies check. In order for satisfies to work in conjunction with literal types, this widening must be prevented. The way that's done here is through contextual typing.
typeFooey={foo: "foo"|"bar"};consterr={foo: "qux"}satisfiesFooey;// error, goodconstok={foo: "bar"}satisfiesFooey;// no error, also good
Normally, properties in an object literal are widened when the type of the object literal is inferred; in this case, ok.foo normally widens to string which would then fail the satisfies check. In order for satisfies to work in conjunction with literal types, this widening must be prevented. The way that's done here is through contextual typing.
Welp I feel pretty dense. I was typing out a whole response about how it felt like this was just a subset of the problem I'd already shared and then suddenly your comment clicked and several other comments made more sense. Just to double check my understanding and in case anyone else comes across this in the future and struggled the same amount, is the following correct?
In my specific case with boolean properties this feels confusing because the widened type you'd normally get, { a: boolean; }, is coincidentally the type specified by Foo. For a union of other literal types, widening the type would produce a primitive type like string, which would not be assignable to any union of literal types because an infinite set of values satisfy string and a union of literal types is by definition finite. So you'd encounter this surprise anytime you used a value of true, false, or something from an enum and checked against a type which specified the widened-but-finite general type (which is what would have been inferred if satisfies wasn't present).
Ricky Munroe (@rimunroe) Yeah, that’s about the gist of it. Hypothetically, if string was a union of all possible strings (impossible because it’s infinite), you’d get the same behavior as with boolean which is treated as true | false.
Activity
Andarist commentedon Jul 28, 2023
This is very specific to
boolean. The "problem" withbooleanis that it's actually a union (true | false). So what you see here, I think, is roughly equivalent to the same situation with a string like here:IIRC, there are some places in the compiler that special case
booleanfor this reason so maybe this issue can be classified as a bug / possible improvement but I'm not sure.rimunroe commentedon Jul 28, 2023
Indeed, which is very unexpected behavior given the description of the feature.
ahejlsberg commentedon Jul 28, 2023
This is working as intended. The
satisfiesoperator provides a contextual type for the left hand expression, and contextual types can (and are intended to) influence their target expression. In this particular case, thebooleancontextual type for the expressiontruecauses the compiler to preserve the literal typetrue(instead of widening it toboolean).I suppose the issue here is that the release notes should have stated this.
rimunroe commentedon Jul 28, 2023
As those notes seem to be the only documentation on this feature, can they be updated? This was extremely surprising to run into
jcalz commentedon Jul 28, 2023
EDIT: oh I see this has already been said here a minute or two ago
As I said in another issue the wording of the description of
satisfiesin the release notes is a bit unfortunate. It most definitely can "change" the type of the expression, via providing a contextual type. I think they mostly meant it won't obliterate the old type in favor of the new type, like an annotation or assertion would.rimunroe commentedon Jul 29, 2023
Aside from the wording in the documentation making me think that the type of the variable wouldn't be affected by its initial value expression having
satisfiesafter it, the thing I was struggling with was why contextual typing was producing a literal type rather than the full union type I'd have expected from eitherFooor from the normal inference you'd get withoutsatisfies.After re-reading the original proposal thread for satisfies (and the feedback reset one) I was still confused, but time I noticed some stuff that prompted me to search for discussions about contextual typing unions. I stumbled on the following extremely relevant part of a comment:
I had no idea that was the case. As a side note, I was originally confused about why Mateusz Burzyński (@Andarist) was bringing up the union nature of booleans, but now it seems they were probably referring to this behavior. I've looked around a bit more but haven't been able to find a clear explanation of why contextually typing unions behaves this way. I've been reading and rereading stuff for a while though so my eyes may have glazed over a bit.
If someone can provide more info about this or point me at some documentation1 or relevant discussion about this that I've missed I'd appreciate it. I suppose it's like a lot of things in the type system where it's something that makes possible the things that I do daily but that I've never noticed it because in other cases it's what I want. I found it totally surprising and unintuitive in this case though.
I'm satisfied that this behavior isn't a bug now, so I'm closing the issue. It would be nice if folks would consider a documentation update to make it clearer that because
satisfiesprovides contextual typing its presence in a declaration can affect the type of a variable.1. I didn't notice anything in the existing handbook page on type inference & contextual typing which covered this.
fatcerberus commentedon Jul 29, 2023
Ricky Munroe (@rimunroe) Here's a good illustrative example:
Normally, properties in an object literal that are initialized with a primitive literal are widened when the type of the object is inferred; in this case,
ok.foonormally widens tostringwhich would then fail thesatisfiescheck. In order forsatisfiesto work in conjunction with literal types, this widening must be prevented. The way that's done here is through contextual typing.rimunroe commentedon Jul 29, 2023
Welp I feel pretty dense. I was typing out a whole response about how it felt like this was just a subset of the problem I'd already shared and then suddenly your comment clicked and several other comments made more sense. Just to double check my understanding and in case anyone else comes across this in the future and struggled the same amount, is the following correct?
In my specific case with boolean properties this feels confusing because the widened type you'd normally get,
{ a: boolean; }, is coincidentally the type specified byFoo. For a union of other literal types, widening the type would produce a primitive type likestring, which would not be assignable to any union of literal types because an infinite set of values satisfystringand a union of literal types is by definition finite. So you'd encounter this surprise anytime you used a value oftrue,false, or something from an enum and checked against a type which specified the widened-but-finite general type (which is what would have been inferred ifsatisfieswasn't present).jcalz commentedon Jul 29, 2023
Also see #10676 for the basic rules for when literal types are and are not widened
fatcerberus commentedon Jul 29, 2023
Ricky Munroe (@rimunroe) Yeah, that’s about the gist of it. Hypothetically, if
stringwas a union of all possible strings (impossible because it’s infinite), you’d get the same behavior as withbooleanwhich is treated astrue | false.rimunroe commentedon Jul 29, 2023
Thank you so much to everyone for the patient explanations!