Description
String literals currently only acquire a string literal type if they are contextually typed by another string literal or a union of string literals. They are then comparable and assertable to any string-like type (i.e. string
, another string literal type, and any union with a string literal type inside of it).
By and large, this gives us the behavior you want; however, there are some problems with this approach.
Problems
switch
/case
This is valid, which seems bad:
let x: "foo" = "foo";
// ...
switch (x) {
case "bar":
console.log("wat");
}
When can x
be "bar"
? Without bypassing the type system, clearly never.
Equality
This is valid, which seems bad:
let x: "foo";
// ...
if (x === "bar") {
}
This is similar to the switch
statement example. When will this condition be true? Again, without fooling the type system, it can't be.
Type assertions
These are both valid, which seems bad:
let x = "hello" as "world";
let y = <"bar" | "baz">"foo";
This is where we allow you to lie to the type system. This is probably way too lenient, so I'd say that this is bad.
Solutions?
Most of the solutions here would require us to tighten our rules regarding allowing these operations between string-like entities. Essentially, we would need to capture some of the same behavior originally proposed in the pull request about a new "comparable" type relationship (#5517).
Widening
Why don't we have every string literal start out with a string literal type and widen to string
when needed? We could certainly do that, but the question is when does a string literal type need to be widened?
To quote @JsonFreeman on #5300 (comment), widening as it would stand today would be a problem for type argument inference:
I think the biggest issue with the widening approach is that types are widened after type argument inference, which is an issue we have discussed before. It makes it so that any string literals that get inferred are automatically widened to string, which is a little unfortunate.
Here's the problem Jason was talking about
declare function f<T>(x: T): T;
let x = f("hello");
Here, we would always widen to string
after picking a type for T
. And even if we didn't, we'd still have other issues:
declare function f<T>(x: T, y: T): T;
let x = f("hello", "world");
When we try to figure out what T
should be, we'd have two different types: "hello"
and "world
. Usually, when trying to infer T
given the choice between something like number
and string
, we'll error. We could change this behavior, and while it might be more desirable to infer "hello" | "world"
here, it would certainly be less consistent.
Contextually type case
expressions
For the switch
/case
example, we could contextually type each case
clause expression by the type of the switch
expression:
let x: "foo" | "bar;
// ...
switch (x) {
case "baz:": // <- error: type '"baz"' is not compatible with '"foo" | "bar"'
console.log("wat");
}
But what about the other way around? We can't contextually type in both directions, because that would create a circularity. A case
clause expression would try to grab a contextual type from the switch
expression, which would try to grab the contextual type from its case
clauses... etc.
So we could just make things unidirectional, which still has undesirable results
let x: "foo" | "bar;
// ...
switch ("baz") {
case x: // <- okay: type '"foo" | "bar"' is compatible with 'string'
console.log("wat");
}
And admittedly, that code is less likely to be written, but really, what is fundamentally different about the first switch (x)
example and the following?
let x: "foo" | "bar";
// ...
if (x === "baz") { // <- okay: type '"foo" | "bar"' is compatible with 'string'
console.log("wat");
}
Clearly you want an error here too, so it would be kind of weird to specially treat switch
statements, but not equality comparisons.
Come up with a some new flow of information, like contextual typing
For equality checks, it'd be nice to have something that can flow both ways. For instance, in this example
let x: "foo" | "bar";
// ...
if (x === "baz") {
console.log("wat");
}
you want "baz"
to grab the type of x
, independent of this type relation, and apply the same sort of information towards creating a string literal type as you would given a contextual type. Since x
would have the type of "foo" | "bar"
, which is a union containing string literal types, "baz"
would acquire its string literal type for the purpose of this check.
You wouldn't end up catching the following
if ("hello" === "world") {
// ...
}
because each side would just have type string
, and inform the other side with the type string
. But you can already do that today, and this is a fairly silly case anyway, so it probably doesn't matter.
The biggest question is how we plan to implement this. It would certainly need a proposal.
Come up with some ad hoc checks
We could come up with some weak checks in these positions to figure out if the user is likely making an error. This would need a proposal.