as enum
assertion for object literals ยท Issue #60790 ยท microsoft/TypeScript (original) (raw)
๐ 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)