Allow identifiers in types to resolve to unit-typed values #29130
Labels
Awaiting More Feedback
This means we'd like to hear from more people who would be helped by this feature
Suggestion
An idea for TypeScript
Search Terms
constant type literal, Cannot find name, type identifier, unit value
Suggestion
When declaring a constant like
const UNSPECIFIED = "UNSPECIFIED";
, I'd like the identifierUNSPECIFIED
to be usable as a type (equivalent to the"UNSPECIFIED"
literal type), just like how you can usenull
, string literals, and enum values as types. There are lots of workarounds and alternatives, but all of them have caused confusion or unease within my team, and I think it's possible to make this straightforward syntax work.As mentioned in #6151 (comment) , simply declaring
UNSPECIFIED
in the type namespace would be a breaking change, so my suggestion is to instead extend type name resolution to look in the value namespace as a fallback, which is backcompat.Here's my attempt at formalizing this behavior:
ID
be the identifier's name.ID
. If a type with that name exists, use it.ID
.ID
is a unit type (string literal, enum value, etc), then treat this node as if it was writtentypeof ID
.ID
's type is not a unit type, give the error message "Cannot use non-unit value ID as a type".(I understand that there may be implementation roadblocks with this approach, just throwing it out there as a suggestion in case it is reasonable like I'm hoping.)
Some related improvements that would also be nice:
Cannot find name "UNSPECIFIED"
to something like"UNSPECIFIED" is a value, not a type"
would be really helpful. This is already filed as More poor errors with value/type/namespace confusion #27630 . It would be especially helpful if the error message pointed to an officially-recommended workaround.Events.Pause
would be usable as a type, and if it was, there would be a pattern for a "union enum" without special language support. It would also allow accessing constants from nested objects (as long as the constant has a literal type) andimport *
statements. You could even go so far as to say "any expresssionE
is a shorthand fortypeof E
", though I think for complex expressions like function invocations, it could be confusing.This proposal requires the relevant values to have literal types, which is mostly true already. Some prior discussions and future suggestions around that: #6167, #10676, #26979 .
Use Cases
See the code snippets below for concrete use cases of literal types in general, though I imagine those are fairly well-understood.
My team is in the process of everyone learning TypeScript and porting a large JavaScript codebase to TypeScript, and this has been one of the bigger issues we've run into. The general advice we've concluded on is "TypeScript doesn't handle constants as types very well; enums work a lot better".
Current approaches and their shortcomings
Use an enum
Switching from enum-style constants to actual enums is helpful, but there are some shortcomings:
UNSPECIFIED
example. We won't always want a list of choices, sometimes we just want a single special value that we can compare against.Use
typeof UNSPECIFIED
as the typeFor example, you could write
type MaybeFolder = Folder | typeof UNSPECIFIED
. This works, but has been confusing enough in practice that I think it's best to avoid, mostly because people feel thattypeof UNSPECIFIED
should bestring
. It generally has a feel of advanced/mysterious TypeScript trickery. It makes sense if you have deep knowledge of how TypeScript works, but it's not intuitive or clear.Explicitly define both a value and a type
There are a few ways to write it, but here's one example:
This is nice in that it's self-contained, but it's doubly-confusing because not only does it use the
typeof
trick, it uses the surprising fact that you can export the same name twice, once in the value namespace and once in the type namespace. We're currently using this pattern for a small number of shared constants, but it seems awkward for people to write new instances of it.Always use a string literal type instead of a constant value
Just using
"UNSPECIFIED"
as the type works out pretty well, but has some disadvantages:Use
null
In some cases,
null
can be used to denote an alternate value, e.g.Folder | null
. This is concise, but makes the intentions less clear.Examples
This can be used to concisely create a safer
null
variant:Similarly, it can be used to extend enums with ad-hoc values:
It can also be used for enum-like use cases for legacy code that doesn't yet use enums:
To demonstrate a few examples of name resolution:
Checklist
My suggestion meets these guidelines:
The text was updated successfully, but these errors were encountered: