microsoft / microsoft/TypeScript
Allow identifiers in types to resolve to unit-typed values
Personne n'a encore pris cette issue.
- Langage dominant
- Go
- Étoiles
- 111k
- Forks
- 14.3k
- Merge moyen
- 2 j 4 h
- PR mergées (30 j)
- 132
Description
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 identifier UNSPECIFIED to be usable as a type (equivalent to the "UNSPECIFIED" literal type), just like how you can use null, 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 https://github.com/Microsoft/TypeScript/issues/6151#issuecomment-165840507 , 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:
- When resolving an identifier node in a type context, let
IDbe the identifier's name.- First search the type namespace for the name
ID. If a type with that name exists, use it. - If no type matched, then search the value namespace to see a value exists with name
ID.- If a value matched, determine the value's type.
- If the type of
IDis a unit type (string literal, enum value, etc), then treat this node as if it was writtentypeof ID. - If
ID's type is not a unit type, give the error message "Cannot use non-unit value ID as a type".
- If the type of
- If no value matched, give the error message "Cannot find name ID".
- If a value matched, determine the value's type.
- First search the type namespace for the name
(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:
- Simply improving the error message from
Cannot find name "UNSPECIFIED"to something like"UNSPECIFIED" is a value, not a type"would be really helpful. This is already filed as https://github.com/Microsoft/TypeScript/issues/27630 . It would be especially helpful if the error message pointed to an officially-recommended workaround. - In addition to changing identifier resolution, ideally more complex expressions could also be resolved in this way, particularly property accesses. In my example in https://github.com/Microsoft/TypeScript/issues/17592#issuecomment-449440944 , ideally
Events.Pausewould 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 expresssionEis 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: https://github.com/Microsoft/TypeScript/issues/6167, https://github.com/Microsoft/TypeScript/pull/10676, https://github.com/Microsoft/TypeScript/issues/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:
- This requires changing all usages, including JS files using these constants. We prefer to keep emitted code changes to a minimum when porting JS to TS, and having to update all usages introduces risk.
- It's awkward to make a one-value enum, like with the
UNSPECIFIEDexample. We won't always want a list of choices, sometimes we just want a single special value that we can compare against. - Since enum values are always written as a property access, they're not as concise as constants.
Use typeof UNSPECIFIED as the type
For 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 that typeof UNSPECIFIED should be string. 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:
export const UNSPECIFIED = "UNSPECIFIED";
export type UNSPECIFIED = typeof UNSPECIFIED;
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:
- It doesn't work as well with editor tooling like jump-to-definition.
- It feels wrong/unsafe/ugly, especially to someone not as familiar with TypeScript.
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:
interface Folder {
name: string;
}
const NO_ACCESS = "NO_ACCESS";
function formatFolder(folder: Folder | NO_ACCESS): string {
if (folder === NO_ACCESS) {
return "(Insufficient permissions)"
} else {
return folder.name;
}
}
Similarly, it can be used to extend enums with ad-hoc values:
enum ColorOption {
RED = "RED",
GREEN = "GREEN",
BLUE = "BLUE",
}
const UNSPECIFIED = "UNSPECIFIED";
interface ColorPickerUIState {
colorChoice: ColorOption | UNSPECIFIED;
}
It can also be used for enum-like use cases for legacy code that doesn't yet use enums:
const RED = "RED";
const GREEN = "GREEN";
const BLUE = "BLUE";
const ORANGE = "ORANGE";
const YELLOW = "YELLOW";
const PURPLE = "PURPLE";
type PrimaryColor = RED | GREEN | BLUE;
To demonstrate a few examples of name resolution:
const number = true;
// "number" refers to plain number type, since type names always take precedence.
const x: number = 3;
type T1 = string;
type T2 = string;
function foo() {
const T2 = "T2";
const T3 = "T3";
// Param type resolves to string | string | "T3".
function f(val: T1 | T2 | T3) {}
}
Checklist
My suggestion meets these guidelines:
- This wouldn't be a breaking change in existing TypeScript/JavaScript code
- This wouldn't change the runtime behavior of existing JavaScript code
- This could be implemented without emitting different JS based on the types of the expressions
- This isn't a runtime feature (e.g. library functionality, non-ECMAScript syntax with JavaScript output, etc.)
- This feature would agree with the rest of TypeScript's Design Goals.
Guide de contribution
Ouvrir le guide de contribution
Par où commencer
- Lisez l'issue en entier, puis le guide de contribution du projet.
- Signalez en commentaire que vous la prenez — cela évite que deux personnes fassent le même travail.
- Forkez le dépôt et travaillez sur une branche.
- Ouvrez une pull request qui référence le numéro de l'issue.
Piste de recherche
L’issue ne nomme aucun fichier, test ni point d’entrée. Commencez par localiser la résolution des identifiants dans le contexte de types de TypeScript ainsi que les tests pour les types literal et enum ; la tâche est terminée lorsque les identifiants du value namespace avec des unit types sont résolus comme des types, tandis que les valeurs non-unit et absentes reçoivent les erreurs spécifiées.
Rédigé par le modèle d'indexation à partir du texte de l'issue.
Évaluation
- Stack technique
- typescript
- Domaine
- compilers
- Type d'issue
- Fonctionnalité
- Difficulté
- 5/5
- Temps estimé
- Plus d'une semaine
- Activité
- À l'abandon
- Clarté
- Plutôt claire
- Accessibilité débutants
- 25/100