I have two enums that I would like to union.
enum Color {
RED = 'red',
BLUE = 'blue',
}
enum Shape {
CIRCLE = 'circle',
SQUARE = 'square',
}
However when I declare a union type and attempt to reference a member:
type ColorShape = Color | Shape;
const test: ColorShape = ColorShape.RED;
I get a compile time error:
TS2693: 'ColorShape' only refers to a type, but is being used as a value here.
Why is this? Would an union of enums not also be an enum itself?
CodePudding user response:
It is important to keep in mind the difference in TypeScript between TypeScript types, which exist at compile time and are erased from the emitted JavaScript code; and JavaScript values, which also exist at runtime and appear in the emitted JavaScript code. TypeScript tries to keep track of values and types, but JavaScript only knows about values.
Most of the time when you make a declaration in TypeScript you are either bringing a value into scope or a type into scope but not both:
// values
const foo = 123;
function bar() { }
// types
type Baz = { a: number };
interface Qux { b: string }
In the above, foo
and bar
are values that exist in the emitted JavaScript, while Baz
and Qux
are types that are erased.
And note that values and types have completely different namespaces; you can have a value and a type with the same name, and they don't have to be related to each other. The compiler doesn't confuse them:
// separate namespaces
const Xyz = { z: 1 };
type Xyz = { y: string };
Here Xyz
is the name of a value (an object with a z
property whose value is 1
) and also the name of a type (an object type with a y
property whose value type is string
). These are unrelated. If I write const uvw = Xyz;
then I'm writing value-level code that makes it to JavaScript and therefore the Xyz
is the value. If I write interface Rst extends Xyz { }
then I'm writing type-level code that's erased from JavaScript and therefore the Xyz
is the type.
Okay, so most of the time when you declare some named thing, that thing is either a type or a value but not both. But there are two declarations that do bring into existence both a value and a type of the same name: the class
declaration and the enum
declaration:
// both
class Abc { c = "" }
enum Def { D = "e" }
Here the name Abc
refers to both a class constructor value that you can reference at runtime like new Abc()
, and it also refers to a class instance type that you can use at compile time to talk about class instances like const instance: Abc = ...
. And if you write const instance: Abc = new Abc()
you are referring first to the type and then to the value.
Similarly, the name Def
refers to both an enum object value that you can reference at runtime like Def.D === "e"
, and it also refers to an enum type that is the union of the types of the enum member values like const def: Def = ...
. And if you write const def: Def = Def.D
you are referring first to the type and then to the value.
Now, while class
is part of JavaScript (since ES2015), enum
is not. So when you write an enum
the compiler will essentially downlevel it to some JavaScript. Let's look at your Color
enum:
enum Color {
RED = 'red',
BLUE = 'blue',
}
This more or less compiles to something that acts like
const Color = {
RED: "red",
BLUE: "blue"
}
It's just a plain object. Meanwhile the type Color
is something that acts like
type Color = "red" | "blue";
That's not strictly true because enum
s are "marked" so that you can't just assign a string to them, but it's close enough for our purposes. So when your enum Color
declaration is similar to the combination of the above value and type declarations.
Now, when you write
type ColorShape = Color | Shape;
You are creating a ColorShape
type. But type ColorShape = ...
is not an enum
, and so all you've done is create the type. There is no corresponding value brought into existence here. You've only done half the work in creating a combined enum
-like thing. If you want the corresponding value, you will have to create it manually.
What you want is something that looks like {RED: "red", BLUE: "blue", CIRCLE: "circle", SQUARE: "square"}
. There are multiple ways to take the Color
value and the Shape
value and make such an object.
You could use the Object.assign()
method:
const ColorShape = Object.assign({}, Color, Shape);
// const ColorShape: typeof Color & typeof Shape
Or you could use object spread:
const ColorShape = { ...Color, ...Shape };
/* const ColorShape: {
CIRCLE: Shape.CIRCLE;
SQUARE: Shape.SQUARE;
RED: Color.RED;
BLUE: Color.BLUE;
} */
Either way should work. Once you've done this, now, you can (mostly) use ColorShape
as if it were a combined enum
composed of Color
and Shape
:
const test: ColorShape = ColorShape.RED; // okay
Note that I said "mostly". The enum
declaration also brings a few more types into scope; it actually acts as a namespace
where you can use the dotted path to enum values as types themselves:
interface RedState {
name: string;
color: Color.RED // <-- this is a type
}
But that doesn't happen with either of your ColorShape
definitions:
interface RedState {
name: string;
color: ColorShape.RED // error!
// 'ColorShape' only refers to a type, but is being used as a namespace here.
}
You probably don't care about that. If somehow you do, then the only way I know how to do this is to manually create your ColorShape
namespace and export each type from it:
namespace ColorShape {
export type CIRCLE = typeof ColorShape.CIRCLE;
export type SQUARE = typeof ColorShape.SQUARE;
export type RED = typeof ColorShape.RED;
export type BLUE = typeof ColorShape.BLUE;
}
Not great, and probably not anything you need. But for completeness I figured I'd just show that there's a bit more going on under the hood with enum
s.