Reputation: 175
I am wrapping an enum in a class so I can map its values in TypeScript
enum Color {
RED = 0,
GREEN = 1,
BLUE = 2
}
const MapColor = new EnumMap(Color); // Wrapper
console.log(MapColor.map()) // [{ "key": "RED", "value": 0}, { "key": "GREEN","value": 1}, { "key": "BLUE", "value": 2}]
console.log(MapColor.entries()) // [["RED", 0], ["GREEN", 1], ["BLUE", 2]]
console.log(MapColor.keys()) // ["RED", "GREEN", "BLUE"]
console.log(MapColor.values()) // [0, 1, 2]
Wrapper Class
class EnumMap {
private _map = new Map<string, string | number>();
public enum: any;
constructor(enumeration: any) {
this.init(enumeration);
return this;
}
private init(enumeration: any) {
this._map = new Map<string, string | number>();
for (let key in enumeration) {
if (!isNaN(Number(key))) continue;
const val = enumeration[key] as string | number;
if (val !== undefined && val !== null) this._map.set(key, val);
}
this.enum = enumeration;
}
map = (): Object => Array.from(this._map.entries()).map((m) => ({ key: m[0], value: m[1] }));
entries = (): any[] => Array.from(this._map.entries());
keys = (): any[] => Array.from(this._map.keys());
values = (): any[] => Array.from(this._map.values());
}
I would like to access an enum value directly, I want to do this
MapColor.RED // 0
MapColor.RED.toString() // 'RED'
Is it possible to create dynamic properties to a class? If possible, how would it be done?
Upvotes: 0
Views: 69
Reputation: 329013
ASIDE:
I am ignoring the request that MapColor.RED
be 0
but MapColor.RED.toString()
evaluate to "RED"
. If you want MapColor.RED === Color.RED
to be true
, so that MapColor.RED
is the number
(primitive data type) whose value is 0
, it is completely impossible for you to get "RED"
as its string value, since (0).toString()
is just "0"
. If you want MapColor.RED
to be a Number
(wrapper object), you could do it as Object.assign(Number(0), {toString(){return "RED"})
, but what comes out of that is awful. It kind of acts like 0
(MapColor.RED + 5 === 5
), except when it doesn't, since MapColor.RED === 0
is false
, and someObject[MapColor.RED]
will access the RED
property of someObject
, not the 0
property. So you either cannot do this, or you can but you shouldn't. So I'm pretending it doesn't exist. 🙈
ANSWER:
You can implement such a class by copying the properties of enumeration
directly to this
inside the constructor (or the init()
method, I guess). And you can describe the type of such a class constuctor using generics via intersection. What you cannot do is have the compiler verify that the implementation obeys the description. You will therefore need to use type assertions in several places to tell the compiler that your class instances and class constructor conform to the behavior in question:
class _EnumMap<T extends Record<keyof T, string | number>> {
private _map = new Map<keyof T, T[keyof T]>();
public enum!: T;
constructor(enumeration: T) {
this.init(enumeration);
return this;
}
private init(enumeration: T) {
this._map = new Map<keyof T, T[keyof T]>();
for (let key in enumeration) {
if (!isNaN(Number(key))) continue;
const val = enumeration[key];
if (val !== undefined && val !== null) {
this._map.set(key, val);
(this as any)[key] = val; // set value here
}
}
this.enum = enumeration;
}
map = () => Array.from(this._map.entries())
.map((m) => ({ key: m[0], value: m[1] })) as
Array<{ [K in keyof T]: { key: K, value: T[K] } }[keyof T]>;
entries = () => Array.from(this._map.entries()) as
Array<{ [K in keyof T]: [K, T[K]] }[keyof T]>;
keys = () => Array.from(this._map.keys()) as Array<keyof T>;
values = () => Array.from(this._map.values()) as Array<T[keyof T]>;
}
I have renamed EnumMap
out of the way to _EnumMap
and I restore the EnumMap
name at the end:
type EnumMap<T extends Record<keyof T, string | number>> = _EnumMap<T> & T;
const EnumMap = _EnumMap as
new <T extends Record<keyof T, string | number>>(enumeration: T) => EnumMap<T>;
The renamed EnumMap
is generic in the type T
of the enumeration
object passed to it. Inside init()
I write this[key] = val
, but the compiler does not know that this
should have keys corresponding to key
, so I assert this as any
to loosen up the compiler warnings. I changed the output types of all your existing methods too, to correspond more correctly to what you're getting out of them... but you didn't ask for that, so keep any
and Object
(or probably object
) if you must.
By writing type EnumMap<T> = _EnumMap<T> & T
, I'm saying that an EnumMap<T>
acts both as your original class, and as a T
. So if T
looks sort of like {RED: 0, GREEN: 1, BLUE: 2}
, then EnumMap<T>
will also have those properties. And by writing const EnumMap = _EnumMap as new<T>(enumeration: T) => EnumMap<T>
, I'm saying that the original class constructor has been copied to a value named EnumMap
, and can be treated as a class constructor for instances of the new EnumMap<T>
.
Let's test it:
enum Color {
RED = 0,
GREEN = 1,
BLUE = 2
}
const MapColor = new EnumMap(Color); // Wrapper
console.log(MapColor.map()) // [{ "key": "RED", "value": 0},
// { "key": "GREEN","value": 1}, { "key": "BLUE", "value": 2}]
console.log(MapColor.entries()) // [["RED", 0], ["GREEN", 1], ["BLUE", 2]]
console.log(MapColor.keys()) // ["RED", "GREEN", "BLUE"]
console.log(MapColor.values()) // [0, 1, 2]
console.log(MapColor.RED) // 0
console.log(MapColor.GREEN) // 1
console.log(MapColor.BLUE) // 2
Looks good to me.
Upvotes: 1