Description
π Search Terms
enum, object literal, type-stripping
β Viability Checklist
- 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, new syntax sugar for JS, etc.)
- This isn't a request to add a new utility type: https://github.com/microsoft/TypeScript/wiki/No-New-Utility-Types
- This feature would agree with the rest of our Design Goals: https://github.com/Microsoft/TypeScript/wiki/TypeScript-Design-Goals
Context
While TypeScript already allows declaring runtime enums values:
export enum Compass {
N = "N",
S = "S",
E = "E",
W = "W",
}
This is not standard JavaScript, and does not work in Node.js unless --experimental-transform-types
is passed.
An alternative "pure JS" pattern from the TypeScript handbook is:
export const Compass = {
N: "N",
S: "S",
E: "E",
W: "W",
} as const;
export type Compass = typeof Compass[keyof typeof Compass];
https://www.typescriptlang.org/docs/handbook/enums.html#objects-vs-enums
There are two downsides to this pattern:
typeof X[keyof typeof X]
is both verbose and not beginner friendly.- The type aliases are not nominal, they are a plain union type.
β Suggestion
Introduce some new syntax to TypeScript to help with the object-literal-as-enum pattern.
For example would be allowing as enum
:
export const Compass = {
N: "N",
S: "S",
E: "E",
W: "W",
} as enum;
(from #59658)
An alternative design could be allowing an enum
type annotation on const
variable declarations export const Compass: enum = {...}
.
This annotation would effectively be the same as writing:
/** secret internal type - here to get nominal typing */
declare const enum __Compass__ {
N = "N",
S = "S",
E = "E",
W = "W",
}
export const Compass = {
N: "N" as __Compass__.N,
S: "S" as __Compass__.S,
E: "E" as __Compass__.E,
W: "W" as __Compass__.W,
} as const;
export type Compass = __Compass__;
Rules
The enum
annotation would only be permitted for object literals that are
- in a declarative position
- have compile-time constant key+values
- all values are either strings, numbers, or references to constant strings/numbers.
i.e. the object literal would follow very similar rules that are applied to const enum C {}
syntax
// @ts-expect-error
foo({ a: "a" } as enum);
class C {
// @ts-expect-error
f: enum = {}
}
const o = {
// @ts-expect-error
p: Math.random()
} as enum;
Benefits
- The standard JS of an object lieral with the type-checking of an
enum
- An explicit marker for tools such as linters to provide extra checks (e.g. enum naming conventions)
Downsides
While this object literal as enum pattern is popular in codebases that avoid non-standard runtime syntax it does not have all the features available with enum
syntax such as self-reference during construction.
__proto__: null
is currently not supported #38385 making it difficult to avoid object literals from inheriting non-enum properties resulting in false positives with key in MyEnum
.
π Motivating Example
export const Compass = {
N: "N",
S: "S",
E: "E",
W: "W",
} as enum;
Object.freeze(Compass);
export function reverse(c: Compass): Compass {
if (c === Compass.N) return Compass.S;
if (c === Compass.S) return Compass.N;
if (c === Compass.E) return Compass.W;
if (c === Compass.W) return Compass.E;
throw new Error("unreachable code was run");
}
The above module will work out-of-the-box in Node.js (assuming nodejs/typescript#17).
π» Use Cases
- What do you want to use this for?
Creating an enum like value using standard Object literal syntax with some of the type system benefits that enum
syntax has.
- What shortcomings exist with current approaches?
typeof Foo[keyof typeof Foo]
is not beginner friendly and is not a nominal type
- What workarounds are you using in the meantime?
One workaround is to have a small utility for emulating an enum like nominal type from an object literal (playground). The "literal" & { __key__: val }
trick works but results in noisey types when displayed to the developer (e.g. in an error message)