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

Support TypeScript const enums #13324

Merged
merged 6 commits into from
Aug 3, 2021
Merged
Show file tree
Hide file tree
Changes from 4 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Jump to
Jump to file
Failed to load files.
Diff view
Diff view
89 changes: 89 additions & 0 deletions packages/babel-plugin-transform-typescript/src/const-enum.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,89 @@
import type * as t from "@babel/types";
import type { NodePath } from "@babel/traverse";

import { translateEnumValues } from "./enum";

export default function transpileConstEnum(
path: NodePath<t.TSEnumDeclaration & { const: true }>,
t: typeof import("@babel/types"),
) {
const { name } = path.node.id;

const parentIsExport = path.parentPath.isExportNamedDeclaration();
let isExported = parentIsExport;
if (!isExported && t.isProgram(path.parent)) {
isExported = path.parent.body.some(
stmt =>
t.isExportNamedDeclaration(stmt) &&
!stmt.source &&
stmt.specifiers.some(
spec => t.isExportSpecifier(spec) && spec.local.name === name,
),
);
}

const entries = translateEnumValues(path, t);

if (isExported) {
const obj = t.objectExpression(
entries.map(([name, value]) =>
t.objectProperty(
t.isValidIdentifier(name)
? t.identifier(name)
: t.stringLiteral(name),
value,
),
),
);

if (path.scope.hasOwnBinding(name)) {
(parentIsExport ? path.parentPath : path).replaceWith(
t.expressionStatement(
t.callExpression(
t.memberExpression(t.identifier("Object"), t.identifier("assign")),
[path.node.id, obj],
),
),
);
} else {
path.replaceWith(
t.variableDeclaration("var", [t.variableDeclarator(path.node.id, obj)]),
);
path.scope.registerDeclaration(path);
}

return;
}

const entriesMap = new Map(entries);

// TODO: After fixing https://github.com/babel/babel/pull/11065, we can
// use path.scope.getBinding(name).referencePaths rather than doing
// a full traversal.
path.scope.path.traverse({
Scope(path) {
if (path.scope.hasOwnBinding(name)) path.skip();
},
MemberExpression(path) {
if (!t.isIdentifier(path.node.object, { name })) return;

let key: string;
if (path.node.computed) {
if (t.isStringLiteral(path.node.property)) {
key = path.node.property.value;
} else {
return;
}
} else if (t.isIdentifier(path.node.property)) {
key = path.node.property.name;
} else {
return;
}
if (!entriesMap.has(key)) return;

path.replaceWith(t.cloneNode(entriesMap.get(key)));
},
});

path.remove();
}
8 changes: 4 additions & 4 deletions packages/babel-plugin-transform-typescript/src/enum.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,9 +5,6 @@ import type { NodePath } from "@babel/traverse";

export default function transpileEnum(path, t) {
const { node } = path;
if (node.const) {
throw path.buildCodeFrameError("'const' enums are not supported.");
}

if (node.declare) {
path.remove();
Expand Down Expand Up @@ -105,7 +102,10 @@ type PreviousEnumMembers = {
[name: string]: number | string;
};

function translateEnumValues(path, t) {
export function translateEnumValues(
path: NodePath<t.TSEnumDeclaration>,
t: typeof import("@babel/types"),
): Array<[name: string, value: t.Expression]> {
const seen: PreviousEnumMembers = Object.create(null);
// Start at -1 so the first enum member is its increment, 0.
let prev: number | typeof undefined = -1;
Expand Down
8 changes: 7 additions & 1 deletion packages/babel-plugin-transform-typescript/src/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@ import syntaxTypeScript from "@babel/plugin-syntax-typescript";
import { types as t, template } from "@babel/core";
import { injectInitialization } from "@babel/helper-create-class-features-plugin";

import transpileConstEnum from "./const-enum";
import transpileEnum from "./enum";
import transpileNamespace from "./namespace";
import type { NodePath } from "@babel/traverse";
Expand Down Expand Up @@ -56,6 +57,7 @@ export default declare((api, opts) => {
jsxPragma = "React.createElement",
jsxPragmaFrag = "React.Fragment",
onlyRemoveTypeImports = false,
optimizeConstEnums = false,
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Can we default it to true in next minor? I know we have onlyRemoveTypeImports default to true in Babel 8, but supporting new language features should be opt-in by default, like we did for other ES features.

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I'm not sure we should.

In TypeScript with --isolatedModules, code like this works and logs x:

const enum A {
  x
}

// @ts-expect-error
console.log(A[A.x]);

export {};

However, with optimizeConstEnums it stops working because we don't ignore const anymore.

For the default behavior, I'd prefer to match --isolatedModules as closely as possible where it's easy to do so.

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Interesting, I think it should be a TS issue, they could have optimized the const enums here.

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

TypeScript docs mentions why ambient const enums (ie. declare const enum) don't work with isolatedModules.

When isolatedModules: false, it seems const enum and declare const enum function the same.

When isolatedModules: true, it seems const enum transpiles as if it was just enum since you get the reverse-mapped object. declare const enum transpiles into the usual empty output, because it's expected to be inlined. However, the isolatedModules: true will produce error:

Cannot access ambient const enums when the '--isolatedModules' flag is provided.(2748)

No idea why they only document declare const enum and not const enum. But I assume their reasoning of converting const enum to just enum is to have it work across modules. And I guess they treat declare const enum as an error since you are explicitly saying it is ambient.


Anyway, @nicolo-ribaudo. I'm not familiar with babel code, but did you make sure you also support declare const enum when optimizeConstEnums: true? When isolatedModules: false, declare const enum seems to function the same as const enum, so you can just strip the declare.

Also, if we want to copy TypeScript functionality with isolatedModules: true and optimizeConstEnums: false, should we transpile const enum by treating it as enum ie. transpile it to the reverse-mapped object instead of throwing an error?

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The default Babel behavior is to match --isolatedModules (as mentioned in our docs at the 4th caveat).

Thus, this PR makes it so that by default const enums are compiled to "normal" enums, which also matches the default behavior of your babel-plugin-const-enum plugin. It's implemented in 933ba59 (#13324).

With optimizeConstEnums: true the plugin seems to correctly work: https://github.com/babel/babel/pull/13324/files#diff-d0d0efefe2a94935f80a8f399bbca77e2a0fbbc8e7a3ba2a1a2f07eff7bcb0de. When optimizeConstEnums: false, it produces some broken output:

declare const enum Animal {
  Dog,
  Cat
}
let x: Animal = Animal.Dog;

// becomes

let x = Animal.Dog; 

however, I'm not too much concerned about it because:

  • It's exactly the same output that TS generates with --isolatedMoudles
  • TypeScript will report a type-checking error saying that declare const enum are not supported.

} = opts;

if (!process.env.BABEL_8_BREAKING) {
Expand Down Expand Up @@ -420,7 +422,11 @@ export default declare((api, opts) => {
},

TSEnumDeclaration(path) {
transpileEnum(path, t);
if (optimizeConstEnums && path.node.const) {
transpileConstEnum(path, t);
} else {
transpileEnum(path, t);
}
},

TSImportEqualsDeclaration(path) {
Expand Down

This file was deleted.

Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
export declare enum A {}

;
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
{
"sourceType": "module"
}
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
;
Original file line number Diff line number Diff line change
@@ -1 +1,2 @@
const enum E {}
// With --isolatedModules, TSC ignores the "const" modifier when compiling enums
const enum E {}

This file was deleted.

Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@
// With --isolatedModules, TSC ignores the "const" modifier when compiling enums
var E;

(function (E) {})(E || (E = {}));
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
export const enum A {
x = 3,
y = "f",
z = 4 << 2,
w = y
}

A.x;
A.y;
A.z;
A.w;
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
export var A = {
x: 3,
y: "f",
z: 16,
w: "f"
};
A.x;
A.y;
A.z;
A.w;
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
const enum A {
x = 3,
y = "f",
z = 4 << 2,
w = y
}

A.x;
A.y;
A.z;
A.w;
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@
3;
"f";
16;
"f";
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
declare const enum A { x }

A.x;
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
0;
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
export const enum A { y }

let x = A.y;
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@
export var A = {
y: 0
};
let x = A.y;
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
const enum A { y }

let x = A.y;

export { A };
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
var A = {
y: 0
};
let x = A.y;
export { A };
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
const enum A { x }

{
let A = {};
A.x;
}

A.x;
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
{
let A = {};
A.x;
}
0;
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
const enum A {
x, y
}

A.x;
A["y"];
A.z;
A;
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@
0;
1;
A.z;
A;
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
export const enum A {
x, y
}

export const enum A {
z
}

A.x;
A["y"];
A.z;
A.w;
A;
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
export var A = {
x: 0,
y: 1
};
Object.assign(A, {
z: 0
});
A.x;
A["y"];
A.z;
A.w;
A;
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
const enum A {
x, y
}

const enum A {
z
}

A.x;
A["y"];
A.z;
A.w;
A;
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
0;
1;
0;
A.w;
A;
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@
{
"plugins": [["transform-typescript", { "optimizeConstEnums": true }]],
"sourceType": "module"
}
3 changes: 3 additions & 0 deletions packages/babel-preset-typescript/src/index.js
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,7 @@ export default declare((api, opts) => {
jsxPragma,
jsxPragmaFrag,
onlyRemoveTypeImports,
optimizeConstEnums,
} = normalizeOptions(opts);

const pluginOptions = process.env.BABEL_8_BREAKING
Expand All @@ -21,6 +22,7 @@ export default declare((api, opts) => {
jsxPragma,
jsxPragmaFrag,
onlyRemoveTypeImports,
optimizeConstEnums,
})
: isTSX => ({
allowDeclareFields: opts.allowDeclareFields,
Expand All @@ -29,6 +31,7 @@ export default declare((api, opts) => {
jsxPragma,
jsxPragmaFrag,
onlyRemoveTypeImports,
optimizeConstEnums,
});

return {
Expand Down
14 changes: 13 additions & 1 deletion packages/babel-preset-typescript/src/normalize-options.js
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,12 @@ import { OptionValidator } from "@babel/helper-validator-option";
const v = new OptionValidator("@babel/preset-typescript");

export default function normalizeOptions(options = {}) {
let { allowNamespaces = true, jsxPragma, onlyRemoveTypeImports } = options;
let {
allowNamespaces = true,
jsxPragma,
onlyRemoveTypeImports,
optimizeConstEnums,
} = options;

if (process.env.BABEL_8_BREAKING) {
const TopLevelOptions = {
Expand All @@ -12,6 +17,7 @@ export default function normalizeOptions(options = {}) {
jsxPragma: "jsxPragma",
jsxPragmaFrag: "jsxPragmaFrag",
onlyRemoveTypeImports: "onlyRemoveTypeImports",
optimizeConstEnums: "optimizeConstEnums",
};
v.validateTopLevelOptions(options, TopLevelOptions);
allowNamespaces = v.validateBooleanOption(
Expand All @@ -29,6 +35,11 @@ export default function normalizeOptions(options = {}) {
options.onlyRemoveTypeImports,
true,
);
optimizeConstEnums = v.validateBooleanOption(
TopLevelOptions.optimizeConstEnums,
options.optimizeConstEnums,
true,
);
}

const jsxPragmaFrag = v.validateStringOption(
Expand Down Expand Up @@ -56,5 +67,6 @@ export default function normalizeOptions(options = {}) {
jsxPragma,
jsxPragmaFrag,
onlyRemoveTypeImports,
optimizeConstEnums,
};
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
const enum A { x }

A.x;
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
0;
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
{
"presets": [["typescript", { "optimizeConstEnums": true }]]
}