Skip to content
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

Allow identifiers in types to resolve to unit-typed values #29130

Open
5 tasks done
alangpierce opened this issue Dec 22, 2018 · 0 comments
Open
5 tasks done

Allow identifiers in types to resolve to unit-typed values #29130

alangpierce opened this issue Dec 22, 2018 · 0 comments
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

Comments

@alangpierce
Copy link
Contributor

alangpierce commented Dec 22, 2018

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 #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:

  • When resolving an identifier node in a type context, let ID be 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 ID is a unit type (string literal, enum value, etc), then treat this node as if it was written typeof ID.
        • If ID's type is not a unit type, give the error message "Cannot use non-unit value ID as a type".
      • If no value matched, give the error message "Cannot find name ID".

(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 More poor errors with value/type/namespace confusion #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 Extending string-based enums #17592 (comment) , ideally 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) and import * statements. You could even go so far as to say "any expresssion E is a shorthand for typeof 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:

  • 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 UNSPECIFIED example. 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.
@weswigham weswigham added Suggestion An idea for TypeScript Awaiting More Feedback This means we'd like to hear from more people who would be helped by this feature labels Dec 25, 2018
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
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
Projects
None yet
Development

No branches or pull requests

2 participants