-
Notifications
You must be signed in to change notification settings - Fork 12.5k
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
String literal types #5185
String literal types #5185
Conversation
…ing literal types. In most cases, expressions are interested in the apparent type of the contextual type. For instance: var x = { hasOwnProperty(prop) { /* ... */ }; In the above, 'prop' should be contextually typed as 'string' from the signature of 'hasOwnProperty' in the global 'Object' type. However, in the case of string literal types, we don't want to get the apparent type after fetching the contextual type. This is because the apparent type of the '"onload"' string literal type is the global 'String' type. This has adverse effects in simple assignments like the following: let x: "onload" = "onload"; In this example, the right-hand side of the assignment will grab the type of 'x'. After figuring out the type is "onload", we then get the apparent type which is 'String'. This is problematic because when we then check the assignment itself, 'String's are not assignable to '"onload"'s. So in this case, we grab the contextual type *without* getting its apparent type.
Very cool. Definitely seems highly desirable to find a way to do narrowing/type-guard sort of behavior here: type CardinalDirection = "North" | "East" | "South" | "West";
function move(distance: number, direction: CardinalDirection) {
if(direction === "North") {
// ok
}
else if(direction === "SouthWest") {
// oops, no error at the moment
}
} |
@@ -10403,6 +10433,7 @@ namespace ts { | |||
case SyntaxKind.TemplateExpression: | |||
return checkTemplateExpression(<TemplateExpression>node); | |||
case SyntaxKind.StringLiteral: | |||
return checkStringLiteralExpression(<LiteralExpression>node); | |||
case SyntaxKind.NoSubstitutionTemplateLiteral: |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Not that I want to sound negative but for every string literal, this gets called.
It's a huge slowdown.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
What kind of numbers are you seeing (and on what codebase)?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
If you consider that every string has on average 5 bytes, you're making 1-5 integer checks for every comparison:
I think the VM already optimize for this?
http://jsperf.com/string-vs-int-comparison-1
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
It's useless to speculate about what's fast and what's not. Measure it.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Meh. Measure it in 1 version of Chrome, it changes in the next. You measure it, I've said what I had to say.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
@jbondc 👍 for the benchmark test.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I don't think the original issue was so terrible. You only ran into this case when you were contextually typed by a type with a string literal constituent to begin with. In practice, this is not frequently encountered.
However, @jbondc, I think you'll like the the current implementation a lot better. We now only create a string literal type if the constituent types have a string literal type, not just if their content is equal.
Furthermore, the types are cached in a map of strings to string literal types. Whenever testing the assignability of two string literal types, reference equality kicks in (which is fast).
Additionally, error messages are slightly better because you have a specific literal type to report (i.e. Type '"Foo"' is not assignable to '"Bar"'.
instead of Type 'string' is not assignable to '"Bar"'.
).
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
@jbondc at worst that's probably much faster than trying to resolve the 100 different overloads. Though, I'm going to try to avoid speculating. I wasn't seeing a real perf hit in our benchmarks even with the initial change.
I assume that it was mentioned somewhere in thread and I didn't find it. Can I use string literal constraint for names of properties? I don't see that it works right now (playground) type Columns = 'name' | 'email' | 'age';
interface TableRow {
[key: Columns]: string;
} |
I'm really hoping to one day be able to quickly write ADTs in a succinct manner. something like this:
Today, the compiler is unable to make the jump from |
@johnsoft we're looking into it, but right now we need to get a sense of how to make that play well with certain other language features. |
Good to hear! Is there another issue I could follow that's more closely related to that use-case? I couldn't find anything after a quick search. |
Also, #186. Duh. Can't believe I forgot about that one. In any case, I am waiting on some work where we'll be modifying the way we do type guards. |
Great, thanks for the links! I'm really impressed with how the language is evolving. |
When i stumbled on this feature i found it a very welcome addition to the language, but after having played a little bit with it, and getting it not to work for everything it throwed at it, i wonder what this is really good for. const testFunc = (stuff:"bar")=>{};
testFunc("bar"); // OK
const stuff = "bar";
testFunc(stuff);// not OK After two hours pulling my hair around this (my initial code was more complicated), i finally found out you have to write const stuff = "bar" as "bar"; Being no TS expert but just a normal programmer trying to gain time with the security of a type checking compiler, i find this overly counter-intuitive and complicated. Sorry if this not the place to discuss this, in the case would you be kind to point me to the correct place ? Thank you. |
@vjau this is expected behaviour. You are relying on type inference. And the infered type of |
This is getting off topic - but yeah, I would expect the type of a |
@joewood it is discussed under #10195 and somewhat under #9489 which was closed because it would be a significant breaking change. @vjau there is a better work around (also discussed in #9489) which is to be explicit about the type: const stuff: 'bar' = 'bar'; There are several types that are "TypeScript only" that don't get inferred, right now, literal types are one of those, tuples are another good example |
@kitsonk The issue in #9489 starts off by talking about the type literal of a const but switches to the issue of implicitly typing React style structures. As @ahejlsberg suggests in that issue, the type of a const could be narrowed to the literal type. I haven't seen a good example of where that would break existing code, unless I'm missing something. |
I didn't raise or respond to the issue... I am just pointing out that it addresses the bit off topic where you felt it wasn't being discussed elsewhere and might be a better place to continue the discussion. |
Thanks @kitsonk, I posted on that issue you referenced - #9489 (comment) |
See my response on the breaking change at #9489 (comment). Just to give some background, I'll write a little bit up here. After this PR went in, we heavily discussed an alternate approach of using widening to try to stop incorrect comparisons (e.g. The discussion started here: #6167 The problem is that too much of the time, the type would vanish too quickly or stick around too long. The behavior was looking generally inappropriate and every time we thought we had the perfect idea, it wasn't good enough for some other case. It became a game of whack-a-use-case. This original PR was nice because:
The comparability problem was solved with #5517 and #6196 anyway, so the only real issue is that people want it to be easier to infer literal types, which I absolutely get. |
Thanks for the detailed response @DanielRosenwasser. In my mind a string literal type of one value is an immutable type, and it's paradoxical to assign that type to a mutable variable (at least, semantically). Implicit type inference for literals should therefore always widen when typing a mutable variable or parameter, and likewise an immutable variable should be narrowed as much as possible. It's a shame the #6554 couldn't be made to work, because it kind of seems counter-intuitive right now that a constant expression's type is different to its implicit type used for immutable binding. |
@joewood it is not paradox, as long as they have the same type. |
@felixfbecker right, but I mean it's essentially at that point semantically immutable. You can change its value to another string of the same value. |
Changes Unknown when pulling ea4e21d on stringLiteralTypes into ** on master**. |
String Literal Types
This pull request adds reasonable support for string literal types as proposed in #1003.
A string literal type can be described by a string literal type node, which is otherwise the same lexically as the StringLiteral production in ECMAScript.
A string literal type is a type whose expected value is a string with textual contents equal to that of the string literal type. In other words, there is no difference between
"hello'world"
and'hello\'world'
.String literal types can be used in conjunction with union types to describe a finite set of possible string values:
A string literal type can be considered a subtype of the
string
type. This means that a string literal type is assignable to a plainstring
, but not vice-versa.Because of this distinction, string literals no longer always have the type
string
. Instead, a string literal's type is informed by its contextual type. The rules are the following:string
.Note that intersections of string literals do not affect the type;
"baz"
will have thestring
type below, and the following assignment will fail:String literal types have the same apparent properties as
string
(i.e. the String global type), and are mostly compatible with operators like+
in the same way that astring
is:Specialized signatures are also compatible with string literal types:
Open Questions
string
s to their respective string literals. Is this worthwhile?const
declarations imply that a string literal type should be used?number
to be assignable toenum
; however, this is due to how frequently people use flags in enums. Should we allowstring
to be assignable to a string literal type? I am personally against this idea.