Intrinsic types remove widening on literal types
See original GitHub issueBug Report
š Search Terms
intrinsic, uppercase, lowercase, widening
š Version & Regression Information
Occurs in Nightly (5.0.0-dev.20221118) and every version I tried going back to 4.1.5
āÆ Playground Link
Playground link with relevant code
š» Code
declare const x: boolean;
declare function capitalize<T extends string>(it: T): Capitalize<T>;
// example 1
let greeting1 = "hello";
if(x) {
greeting1 = "goodbye"; // allowed
}
// example 2
let greeting2 = capitalize("hello");
if(x) {
greeting2 = capitalize("goodbye"); // type error
}
This example is adapted from @RyanCavanaughās example here.
š Actual behavior
Reassigning a new string to greeting1
succeeds, but reassigning one to greeting2
fails.
š Expected behavior
The widening behavior for standard literal types (like "hello"
) should not be different from the behavior of the type produced by Capitalize<"hello">
; if the widening behavior were the same, greeting2
would also have its type inferred as string
, and would accept a new string being assigned to it.
Having inconsistent widening like this seems unintuitive and possibly unintentional. Concretely, it also makes it impossible to use an intrinsically-typed value in a let
statement (as shown), which creates a similar problem in @RyanCavanaughās original example in #44268.
Issue Analytics
- State:
- Created 10 months ago
- Comments:14 (9 by maintainers)
Top GitHub Comments
The widening that happens here is completely different though. The wrapping
capitalize
call changes everything here. You always~ get an inferred type from the given expression based on the surrounding context.When assigning a string to a
let
variable the type of this variable becomesstring
when you pass it through a function that can infer a different type based on its arguments then u will get the type returned by that function. It matches your expectations here - otherwise, you wouldnāt use aCapitalize<T>
there asCapitalize<string>
is roughly equivalent to juststring
. So clearly you have wanted to get a literal type there.From that moment the type of that variable is fixed - it wonāt widen on the following assignments. Itās basically the same case as this one:
This one is actually āfixableā by using āautoā types like here (by not annotating the
greeting1
ās type):So, in a way, you could leverage the auto types in your example: TS playground. This would look quite weird though, so I would advise you to just provide an explicit generic argument like here.
Perhaps what youād like to request here is to annotate a variable as āautoā - so you wouldnāt have to add a generic type param and you wouldnāt have to split the initial variable declaration and the initial value assignment
Hey @RyanCavanaugh, sorry to hear you were sick. Welcome back, and I hope you were able to have a nice thanksgiving.
I agree: thereās clearly not a huge amount of pain caused by the current system. My hope would still be that at least some of these edge cases are avoidable, without complicating the underlying rules, but I confess that I canāt think of concrete proposal right now that would definitely improve things enough to justify the breakage.
I think @fatcerberus is right that undoing #24310 would address the OP and wouldnāt produce problems on your āGordian Knotā example ā but it would reintroduce the issue that #24310 was used to solve, and thatās where Iām stumped.
In the example from #23649:
langCodes
is supposed to be an array of string literals, rather thanstring[]
.Had #24310 never been introduced, I think I wouldāve tried to accomplish that in some other way, rather than cueing off of the
extends string
on the generic. E.g., maybe #23649 couldāve been solved with a rule saying that the literal type inferred forK
shouldnāt be widening becauseK
is used in an object key position (in theRecord
mapped type). Iām not saying that wouldāve been an ideal (or even workable) rule, but just that maybe there were potential alternatives before #24310 was added.However, given that #24310 has been introduced, I bet a bunch of people now are relying on getting literal types in all sorts of places where they were previously being widened, so rolling back #24310 now would probably be too disruptive.
I looked in my own code and found a function like this:
In that example, Iām relying on #24310 to give me a literal type for the inferred type of the
__typename
key, and I canāt think of an alternate rule (that doesnāt require any code changes) that would do that.Had #24310 not been introduced, I mightāve proposed addressing that use case like this:
In the above snippet, Iām extending the set of places where
as const
can legally appear; instead of being allowed only on literals, Iām saying itād be allowed onname
becausename
ās type is constrained such that it might have a literal type, and theas const
there would mean ādonāt make the literal type widenā.But, again, now that people are relying on #24310, I doubt the breakage would be justified.
So, idk, maybe Iāll keep noodling on this and, for now, Iāll just open a separate issue for the one concrete bug that I think this thread identified, which is that
foo as typeof foo
should always be a no-op (imo).