how to convert to NotNull by using enum in Kotlin - enums

How can I convert the following code to accepted NotNull inside enum class by using Kotlin?
Note: i'm using this enum between two activity and one activity has 2 adapters.
Here is enum class
enum class Adapterx {
ADAPTER_1,
ADAPTER_2;
companion object {
fun fromOrdinal(ordinal: Int): Adapterx? {
return Adapterx.values().firstOrNull { it.ordinal == ordinal }
}
}
}

Since you cannot restrict the ordinal: Int parameter as you've define it, you have two choices if you receive an ordinal which is not part of the enum, or is out of bounds:
Return a default value
Throw an exception
IMHO both cases are plausible if you document properly the method.
Here's a case where you return just a default value if you ask for an ordinal that does not exist:
class KotlinEnumTest {
enum class Adapterx {
ADAPTER_1,
ADAPTER_2;
companion object {
val defaultValue = ADAPTER_1
fun fromOrdinal(ordinal: Int): Adapterx =
Adapterx.values().getOrElse(ordinal, { _ -> defaultValue })
}
}
#Test fun testEnumOrdinals() {
val resultAdapter1 = Adapterx.fromOrdinal(0)
Assert.assertEquals(Adapterx.ADAPTER_1, resultAdapter1)
val resultAdapter2 = Adapterx.fromOrdinal(1)
Assert.assertEquals(Adapterx.ADAPTER_2, resultAdapter2)
// The following returns the default value ADAPTER_1
val resultOrdinalIndexOutOfBounds = Adapterx.fromOrdinal(2)
Assert.assertEquals(Adapterx.ADAPTER_1, resultOrdinalIndexOutOfBounds)
}
}

Related

kotlin safe conversion from string to enum

I need to convert strings to Enum values, but want a function which returns null if the string is not an enum.
enum class Colors{
Red, Green, Blue
}
I can used Colors.valueOf(testString) provided testString is value, but there will be an exception if it is not valid, and I want a null in that case.
Because I want to this often, an extension function would be ideal. But the extension needs to operate on the class Colors, and not an object of type Colors.
Anyone know how to write such an extension? Ideally one that is generic for any enum class.
It is simple to write a top level function, but I am seeking one that acts as the standard 'method' does
// instead of
val willGetAnException = Colors.valueOf("Yellow") // standard existing fun
val willGetNull = Colors.valueOrNullOf("Orange") // new fun i seek
And ideally one that is generic and works for any enum
You don't want an extension since they must be invoked on an existing object. You want a top-level function. There is a built in one You can use:
/**
* Returns an enum entry with specified name.
*/
#SinceKotlin("1.1")
public inline fun <reified T : Enum<T>> enumValueOf(name: String): T
You can call it by inferring the type, or explicitly:
val a : MyEnumClass = enumValueOf("A")
val b = enumValueOf<MyEnumClass>("B")
However this method is not nullable: it throws java.lang.IllegalArgumentException on unknown values.
But it's easy to mimick it's behavior and have it work for nullable enums with a top level function:
inline fun <reified T : Enum<*>> enumValueOrNull(name: String): T? =
T::class.java.enumConstants.firstOrNull { it.name == name }
Colors.values().find { it.name == "Yellow" }
You can use something like this :
inline fun <reified T : Enum<T>> String.asEnumOrDefault(defaultValue: T? = null): T? =
enumValues<T>().firstOrNull { it.name.equals(this, ignoreCase = true) } ?: defaultValue
Then: "Yellow".asEnumOrDefault(Colors.Green)
Or, if you it can't be infered: "Yellow".asEnumOrDefault<Colors>()
enum class Colors {
BLACK, WHITE, UNKNOWN;
companion object {
// Verbose for illustrative purposes
fun fromOrdinal(ordinal: Int): Colors = values()[ordinal]
fun fromOrdinalOrNull(ordinal: Int): Colors? = values().getOrNull(ordinal)
fun fromOrdinalOrDefault(ordinal: Int): Colors = values().getOrElse(ordinal) { UNKNOWN }
fun fromName(name: String): Colors = valueOf(name.uppercase())
fun fromNameOrNull(name: String): Colors? = values().find { it.name == name.uppercase() }
fun fromNameOrDefault(name: String): Colors = values().find { it.name == name.uppercase() } ?: UNKNOWN
}
}
Given the fact it's not easy to access the Enum value safely in Kotlin, I published a library enum-or-null-kt to provide a collection of shorthand functions which makes you can write code like below:
class Example {
enum class Direction(val az: Int) {
NORTH(0),
EAST(90),
SOUTH(180),
WEST(240)
}
fun printAz01(name: String = "EAST") {
val direction = enumValueOrNull<Direction>(name) ?: Direction.EAST
println("az01=${direction.az}")
}
fun printAz02(name: String = "EAST") {
val direction = name.toEnumOrNull<Direction>() ?: Direction.EAST
println("az02=${direction.az}")
}
fun printName01(az: Int = 0) {
val direction = enumValueOrNull<Direction> {
it.az == az
} ?: Direction.NORTH
println("name03=${direction.name}")
}
fun printName02(ordinal: Int = 0) {
val direction = enumValueOrNull<Direction> {
it.ordinal == ordinal
} ?: Direction.NORTH
println("name03=${direction.name}")
}
}
With it, not only can you access the Enum value with names, but also you can pass an arbitrary higher-order function as a predicate clause. That is convenient when you need to deal with a custom conversion such as JPA attribute converters.
The Kotlin API does not work by simply using <reified T: Enum<T>>. It throws an exception of the type InvocationTargetException. So I pass directly to type: Class<T> by parameter.
private fun <T> enumValueOf(type: Class<T>, enum: String) : T {
return type.enumConstants.first { it.toString() == enum }
}
Using
if (type.isEnum) enumValueOf(#Field.type, value as String)

More fun with Kotlin delegates

If you know Google's experimental Android Architecture Components, you probably know MutableLiveData. Trying to make it a bit more fun to use I came with:
class KotlinLiveData<T>(val default: T) {
val data = MutableLiveData<T>()
operator fun getValue(thisRef: Any?, property: KProperty<*>):T {
return data.value ?: default
}
operator fun setValue(thisRef: Any?, property: KProperty<*>, value:T) {
if (Looper.myLooper() == Looper.getMainLooper()) {
data.value = value
} else {
data.postValue(value)
}
}
}
And then I can:
var name : String by KotlinLiveData("not given")
name = "Chrzęszczybrzęczykiewicz"
But alas - that makes data which is needed i.e. to register Observer inaccessible:
name.data.observe(this, nameObserver) // won't work :(
Any idea if I can get it somehow?
You can access the delegate object of the property and get the MutableLiveData<T> from it:
inline fun <reified R> KProperty<*>.delegateAs<R>(): R? {
isAccessible = true
return getDelegate() as? R
}
Then the usage is:
::name.delegateAs<KotlinLiveData<String>>?.data?.observe(this, nameObserver)
To reference a member property, use this::name or someInstance::name.
This solution requires you to add the Kotlin reflection API, kotlin-reflect, as a dependency to your project. Also, due to the type erasure, the .delegateAs<KotlinLiveData<String>> call is not type-safe: it can only check that the delegate is KotlinLiveData<*> but not that its type argument is String.
Thanks to hotkey's solution, here's some better code:
class KotlinLiveData<T>(val default: T, val liveData : MutableLiveData<T>? = null) {
val data = liveData ?: MutableLiveData<T>()
operator fun getValue(thisRef: Any?, property: KProperty<*>):T {
return data.value ?: default
}
operator fun setValue(thisRef: Any?, property: KProperty<*>, value:T) {
if (Looper.myLooper() == Looper.getMainLooper()) {
data.value = value
} else {
data.postValue(value)
}
}
}
inline fun <reified R> KMutableProperty0<*>.getLiveData(): MutableLiveData<R> {
isAccessible = true
return (getDelegate() as KotlinLiveData<R>).data
}
inline fun <reified R> KMutableProperty0<*>.observe(owner: LifecycleOwner, obs : Observer<R>) {
isAccessible = true
(getDelegate() as KotlinLiveData<R>).data.observe(owner,obs)
}
Now I can:
someViewModel::name.observe(myActivity, Observer<String>{...})
with
someViewModel.name = "Kowalski, Leon"
working as expected
This class enables using LiveData with Android Data Binding out of the box.
the simplest way you can achieve is make the delegator to a field, for example:
#JvmField val dataOfName = KotlinLiveData("not given")
var name : String by dataOfName
then you can using live data in the class, for example:
dataOfName.data.observe(this, nameObserver)
name = "Chrzęszczybrzęczykiewicz"
OR you can write some syntax suglar, for example:
var name : String by live("not given").observe(this, nameObserver)
Note you can make nameObserver lazily too, for example:
val observers by lazy{mutableListOf<Observer>()}
var name : String by live("not given").observe(this){data->
observers.forEach{it.dataChanged(data)}
}
then you can do something like as below:
observers+= nameObserver;
name = "Chrzęszczybrzęczykiewicz"
observers-= nameObserver;

How to write a reusable transform for String to Enum value across a group of Enum classes? (Kotlin)

I have a group >5 of Enum classes that take String parameter in its values, and I want to have simple code for all these Enum classes to convert from a String field in JSON object.
enum class Religiousness(val jsonStr: String, val resID: Int) {
NotAtAll("none", R.string.not_religious),
Somewhat("somewhat", R.string.somewhat_religious),
Very("very", R.string.very_religious),
;
override fun toString() = jsonStr
fun displayString(res: Resources) = res.getString(resID)
}
I want to be able to write code like this
fun JsonConvertStrToEnum(enumClass: Class<Enum<*>>, str: String): Enum<*> {
for (enumval in enumClass.enumConstants) {
if ((enumval as IJsonStringConvertible).jsonStr() == str)
return enumval
}
throw IllegalArgumentException("Gave an invalid enum value for class ${enumClass.canonicalName}")
}
I am having a hard time figuring out if IJsonStringConvertible can work, and what its definition would be, and how to implement it in the Enum value instances. Any advice?
Update: I have now written the converter as this. Is this the best way? Can I also express that the return value is a subtype of the parameter so don't need to cast return value?
fun JsonConvertStrToEnum(enumClass: Class<out Enum<*>>, str: String): Enum<*> {
for (enumval in enumClass.enumConstants) {
if (enumval.toString() == str)
return enumval
}
throw IllegalArgumentException("Gave an invalid enum value for class ${enumClass.canonicalName}")
}
Enums as other classes can implement interfaces like so:
interface IJsonStringConvertible {
val jsonStr:String
}
enum class Religiousness(override val jsonStr: String, val resID: Int) : IJsonStringConvertible {
NotAtAll("none", R.string.not_religious),
Somewhat("somewhat", R.string.somewhat_religious),
Very("very", R.string.very_religious),
;
override fun toString() = jsonStr
fun displayString(res: Resources) = res.getString(resID)
}
Which would then be used as:
for (enumval in enumClass.enumConstants) {
if ((enumval as IJsonStringConvertible).jsonStr == str)
return enumval
}
However the above lookup can be expensive (if used millions of times). Take a look at the reverse lookup question to find out how to do it more efficiently.
If it helps anyone, here's the final version in my production app.
fun <EnumT : Enum<EnumT>> ConvertStrToEnum(enumClass: Class<EnumT>, str: String?): EnumT? {
if (str == null)
return null
for (enumval in enumClass.enumConstants) {
if (enumval.toString() == str)
return enumval
}
throw IllegalArgumentException("Gave an invalid enum value for class ${enumClass.canonicalName}")
}
fun <EnumT : Enum<EnumT> > ConvertStrArrayToEnumSet(enumClass: Class<EnumT>, array: List<String>?) : EnumSet<EnumT> {
val set = EnumSet.noneOf(enumClass)
array?.forEach { value -> set.add(ConvertStrToEnum(enumClass, value)) }
return set
}

Effective Enums in Kotlin with reverse lookup?

I'm trying to find the best way to do a 'reverse lookup' on an enum in Kotlin. One of my takeaways from Effective Java was that you introduce a static map inside the enum to handle the reverse lookup. Porting this over to Kotlin with a simple enum leads me to code that looks like this:
enum class Type(val value: Int) {
A(1),
B(2),
C(3);
companion object {
val map: MutableMap<Int, Type> = HashMap()
init {
for (i in Type.values()) {
map[i.value] = i
}
}
fun fromInt(type: Int?): Type? {
return map[type]
}
}
}
My question is, is this the best way to do this, or is there a better way? What if I have several enums that follow a similar pattern? Is there a way in Kotlin to make this code more re-usable across enums?
First of all, the argument of fromInt() should be an Int, not an Int?. Trying to get a Type using null will obviously lead to null, and a caller shouldn't even try doing that. The Map has also no reason to be mutable. The code can be reduced to:
companion object {
private val map = Type.values().associateBy(Type::value)
fun fromInt(type: Int) = map[type]
}
That code is so short that, frankly, I'm not sure it's worth trying to find a reusable solution.
we can use find which Returns the first element matching the given predicate, or null if no such element was found.
companion object {
fun find(value: Int): Type? = Type.values().find { it.value == value }
}
Another option, that could be considered more "idiomatic", would be the following:
companion object {
private val map = Type.values().associateBy(Type::value)
operator fun get(value: Int) = map[value]
}
Which can then be used like Type[type].
It makes not much sense in this case, but here is a "logic extraction" for #JBNized's solution:
open class EnumCompanion<T, V>(private val valueMap: Map<T, V>) {
fun fromInt(type: T) = valueMap[type]
}
enum class TT(val x: Int) {
A(10),
B(20),
C(30);
companion object : EnumCompanion<Int, TT>(TT.values().associateBy(TT::x))
}
//sorry I had to rename things for sanity
In general that's the thing about companion objects that they can be reused (unlike static members in a Java class)
If you have a lot of enums, this might save a few keystrokes:
inline fun <reified T : Enum<T>, V> ((T) -> V).find(value: V): T? {
return enumValues<T>().firstOrNull { this(it) == value }
}
Use it like this:
enum class Algorithms(val string: String) {
Sha1("SHA-1"),
Sha256("SHA-256"),
}
fun main() = println(
Algorithms::string.find("SHA-256")
?: throw IllegalArgumentException("Bad algorithm string: SHA-256")
)
This will print Sha256
I found myself doing the reverse lookup by custom, hand coded, value couple of times and came of up with following approach.
Make enums implement a shared interface:
interface Codified<out T : Serializable> {
val code: T
}
enum class Alphabet(val value: Int) : Codified<Int> {
A(1),
B(2),
C(3);
override val code = value
}
This interface (however strange the name is :)) marks a certain value as the explicit code. The goal is to be able to write:
val a = Alphabet::class.decode(1) //Alphabet.A
val d = Alphabet::class.tryDecode(4) //null
Which can easily be achieved with the following code:
interface Codified<out T : Serializable> {
val code: T
object Enums {
private val enumCodesByClass = ConcurrentHashMap<Class<*>, Map<Serializable, Enum<*>>>()
inline fun <reified T, TCode : Serializable> decode(code: TCode): T where T : Codified<TCode>, T : Enum<*> {
return decode(T::class.java, code)
}
fun <T, TCode : Serializable> decode(enumClass: Class<T>, code: TCode): T where T : Codified<TCode> {
return tryDecode(enumClass, code) ?: throw IllegalArgumentException("No $enumClass value with code == $code")
}
inline fun <reified T, TCode : Serializable> tryDecode(code: TCode): T? where T : Codified<TCode> {
return tryDecode(T::class.java, code)
}
#Suppress("UNCHECKED_CAST")
fun <T, TCode : Serializable> tryDecode(enumClass: Class<T>, code: TCode): T? where T : Codified<TCode> {
val valuesForEnumClass = enumCodesByClass.getOrPut(enumClass as Class<Enum<*>>, {
enumClass.enumConstants.associateBy { (it as T).code }
})
return valuesForEnumClass[code] as T?
}
}
}
fun <T, TCode> KClass<T>.decode(code: TCode): T
where T : Codified<TCode>, T : Enum<T>, TCode : Serializable
= Codified.Enums.decode(java, code)
fun <T, TCode> KClass<T>.tryDecode(code: TCode): T?
where T : Codified<TCode>, T : Enum<T>, TCode : Serializable
= Codified.Enums.tryDecode(java, code)
Another example implementation. This also sets the default value (here to OPEN) if no the input matches no enum option:
enum class Status(val status: Int) {
OPEN(1),
CLOSED(2);
companion object {
#JvmStatic
fun fromInt(status: Int): Status =
values().find { value -> value.status == status } ?: OPEN
}
}
True Idiomatic Kotlin Way. Without bloated reflection code:
interface Identifiable<T : Number> {
val id: T
}
abstract class GettableById<T, R>(values: Array<R>) where T : Number, R : Enum<R>, R : Identifiable<T> {
private val idToValue: Map<T, R> = values.associateBy { it.id }
operator fun get(id: T): R = getById(id)
fun getById(id: T): R = idToValue.getValue(id)
}
enum class DataType(override val id: Short): Identifiable<Short> {
INT(1), FLOAT(2), STRING(3);
companion object: GettableById<Short, DataType>(values())
}
fun main() {
println(DataType.getById(1))
// or
println(DataType[2])
}
A variant of some previous proposals might be the following, using ordinal field and getValue :
enum class Type {
A, B, C;
companion object {
private val map = values().associateBy(Type::ordinal)
fun fromInt(number: Int): Type {
require(number in 0 until map.size) { "number out of bounds (must be positive or zero & inferior to map.size)." }
return map.getValue(number)
}
}
}
A slightly extended approach of the accepted solution with null check and invoke function
fun main(args: Array<String>) {
val a = Type.A // find by name
val anotherA = Type.valueOf("A") // find by name with Enums default valueOf
val aLikeAClass = Type(3) // find by value using invoke - looks like object creation
val againA = Type.of(3) // find by value
val notPossible = Type.of(6) // can result in null
val notPossibleButThrowsError = Type.ofNullSave(6) // can result in IllegalArgumentException
// prints: A, A, 0, 3
println("$a, ${a.name}, ${a.ordinal}, ${a.value}")
// prints: A, A, A null, java.lang.IllegalArgumentException: No enum constant Type with value 6
println("$anotherA, $againA, $aLikeAClass $notPossible, $notPossibleButThrowsError")
}
enum class Type(val value: Int) {
A(3),
B(4),
C(5);
companion object {
private val map = values().associateBy(Type::value)
operator fun invoke(type: Int) = ofNullSave(type)
fun of(type: Int) = map[type]
fun ofNullSave(type: Int) = map[type] ?: IllegalArgumentException("No enum constant Type with value $type")
}
}
Based on your example, i might suggest removing the associated value and just use the ordinal which is similar to an index.
ordinal - Returns the ordinal of this enumeration constant (its position in its enum declaration, where the initial constant is assigned an ordinal of zero).
enum class NavInfoType {
GreenBuoy,
RedBuoy,
OtherBeacon,
Bridge,
Unknown;
companion object {
private val map = values().associateBy(NavInfoType::ordinal)
operator fun get(value: Int) = map[value] ?: Unknown
}
}
In my case i wanted to return Unknown if the map returned null. You could also throw an illegal argument exception by replacing the get with the following:
operator fun get(value: Int) = map[value] ?: throw IllegalArgumentException()
An approach that reuses code:
interface IndexedEnum {
val value: Int
companion object {
inline fun <reified T : IndexedEnum> valueOf(value: Int) =
T::class.java.takeIf { it.isEnum }?.enumConstants?.find { it.value == value }
}
}
Then the enums can be made indexable:
enum class Type(override val value: Int): IndexedEnum {
A(1),
B(2),
C(3)
}
and reverse searched like so:
IndexedEnum.valueOf<Type>(3)
There is a completely generic solution that
Does not use reflection, Java or Kotlin
Is cross-platform, does not need any java
Has minimum hassle
First, let's define our interfaces as value field is not inherent to all enums:
interface WithValue {
val value: Int
}
interface EnumCompanion<E> where E: Enum<E> {
val map: Map<Int, E>
fun fromInt(type: Int): E = map[type] ?: throw IllegalArgumentException()
}
Then, you can do the following trick
inline fun <reified E> EnumCompanion() : EnumCompanion<E>
where E : Enum<E>, E: WithValue = object : EnumCompanion<E> {
override val map: Map<Int, E> = enumValues<E>().associateBy { it.value }
}
Then, for every enum you have the following just works
enum class RGB(override val value: Int): WithValue {
RED(1), GREEN(2), BLUE(3);
companion object: EnumCompanion<RGB> by EnumCompanion()
}
val ccc = RGB.fromInt(1)
enum class Shapes(override val value: Int): WithValue {
SQUARE(22), CIRCLE(33), RECTANGLE(300);
companion object: EnumCompanion<Shapes> by EnumCompanion()
}
val zzz = Shapes.fromInt(33)
As already mentioned, this is not worth it unless you have a lot of enums and you really need to get this generic.
Came up with a more generic solution
inline fun <reified T : Enum<*>> findEnumConstantFromProperty(predicate: (T) -> Boolean): T? =
T::class.java.enumConstants?.find(predicate)
Example usage:
findEnumConstantFromProperty<Type> { it.value == 1 } // Equals Type.A
val t = Type.values()[ordinal]
:)

Add functions to an Enum

Is it possible to add functions to an Enum type in TypeScript?
for example:
enum Mode {
landscape,
portrait,
// the dream...
toString() { console.log(this); }
}
Or:
class ModeExtension {
public toString = () => console.log(this);
}
enum Mode extends ModeExtension {
landscape,
portrait,
}
Of course the toString() function would contain something like a switch But a use-case would flow along the lines of:
class Device {
constructor(public mode:Mode) {
console.log(this.mode.toString());
}
}
I understand why extending an enum might be a strange thing, just wondering if it is possible.
You can either have a class that is separate to the Enum and use it to get things you want, or you can merge a namespace into the Enum and get it all in what looks like the same place.
Mode Utility Class
So this isn't exactly what you are after, but this allows you to encapsulate the "Mode to string" behaviour using a static method.
class ModeUtil {
public static toString(mode: Mode) {
return Mode[mode];
}
}
You can use it like this:
const mode = Mode.portrait;
const x = ModeUtil.toString(mode);
console.log(x);
Mode Enum/Namespace Merge
You can merge a namespace with the Enum in order to create what looks like an Enum with additional methods:
enum Mode {
X,
Y
}
namespace Mode {
export function toString(mode: Mode): string {
return Mode[mode];
}
export function parse(mode: string): Mode {
return Mode[mode];
}
}
const mode = Mode.X;
const str = Mode.toString(mode);
alert(str);
const m = Mode.parse(str);
alert(m);
You can get the string value of an non-const enum by using square brackets:
class Device {
constructor(public mode:Mode) {
console.log(Mode[this.mode]);
}
}
You can also put some enum-specific util functions into the enum, but that's just like static class members:
enum Mode {
landscape,
portrait
}
namespace Mode {
export function doSomething(mode:Mode) {
// your code here
}
}
Convert your enum to the enum pattern. I find this is a better practice in general for many languages, since otherwise you restrict encapsulation options for your type. The tendency is to switch on the enum value, when really any data or functionality that is dependent on the particular enum value should just go into each instance of the enum. I've added some more code to demonstrate.
This might not work if you are particularly dependent on the underlying enum values. In that case you would need to add a member for the old values and convert places that need it to use the new property.
class Mode {
public static landscape = new Mode(1920, 1080);
public static portrait = new Mode(1080, 1920);
public get Width(): number { return this.mWidth; }
public get Height(): number { return this.mHeight; }
// private constructor if possible in a future version of TS
constructor(
private mWidth: number,
private mHeight: number
) {
}
public GetAspectRatio() {
return this.mWidth / this.mHeight;
}
}
An addition to Fenton's solution.
If you want to use this enumerator in another class, you need to export both the enum and the namespace. It would look like this:
export enum Mode {
landscape,
portrait
}
export namespace Mode {
export function toString(mode: Mode): string {
return Mode[mode];
}
}
Then you just import the mode.enum.ts file in your class and use it.
can make enum like by private constructor and static get return object
export class HomeSlideEnum{
public static get friendList(): HomeSlideEnum {
return new HomeSlideEnum(0, "friendList");
}
public static getByOrdinal(ordinal){
switch(ordinal){
case 0:
return HomeSlideEnum.friendList;
}
}
public ordinal:number;
public key:string;
private constructor(ordinal, key){
this.ordinal = ordinal;
this.key = key;
}
public getTitle(){
switch(this.ordinal){
case 0:
return "Friend List"
default :
return "DChat"
}
}
}
then later can use like this
HomeSlideEnum.friendList.getTitle();
ExtendedEnum Class
I always loved the associated types in Swift and I was looking to extend Typescript's enum basic functionality. The idea behind this approach was to keep Typescript enums while we add more properties to an enum entry. The proposed class intends to associate an object with an enum entry while the basic enum structure stays the same.
If you are looking for a way to keep vanilla Typescript enums while you can add more properties to each entry, this approach might be helpful.
Input
enum testClass {
foo = "bar",
anotherFooBar = "barbarbar"
}
Output
{
entries: [
{
title: 'Title for Foo',
description: 'A simple description for entry foo...',
key: 'foo',
value: 'bar'
},
{
title: 'anotherFooBar',
description: 'Title here falls back to the key which is: anotherFooBar.',
key: 'anotherFooBar',
value: 'barbarbar'
}
]
}
Implementation
export class ExtendedEnum {
entries: ExtendedEnumEntry[] = []
/**
* Creates an instance of ExtendedEnum based on the given enum class and associated descriptors.
*
* #static
* #template T
* #param {T} enumCls
* #param {{ [key in keyof T]?: EnumEntryDescriptor }} descriptor
* #return {*} {ExtendedEnum}
* #memberof ExtendedEnum
*/
static from<T extends Object>(enumCls: T, descriptor: { [key in keyof T]?: EnumEntryDescriptor }): ExtendedEnum {
const result = new ExtendedEnum()
for (const anEnumKey of Object.keys(enumCls)) {
if (isNaN(+anEnumKey)) { // skip numerical keys generated by js.
const enumValue = enumCls[anEnumKey]
let enumKeyDesc = descriptor[anEnumKey] as EnumEntryDescriptor
if (!enumKeyDesc) {
enumKeyDesc = {
title: enumValue
}
}
result.entries.push(ExtendedEnumEntry.fromEnumEntryDescriptor(enumKeyDesc, anEnumKey, enumValue))
}
}
return result
}
}
export interface EnumEntryDescriptor {
title?: string
description?: string
}
export class ExtendedEnumEntry {
title?: string
description?: string
key: string
value: string | number
constructor(title: string = null, key: string, value: string | number, description?: string) {
this.title = title ?? key // if title is not provided fallback to key.
this.description = description
this.key = key
this.value = value
}
static fromEnumEntryDescriptor(e: EnumEntryDescriptor, key: string, value: string | number) {
return new ExtendedEnumEntry(e.title, key, value, e.description)
}
}
Usage
enum testClass {
foo = "bar",
anotherFooBar = "barbarbar"
}
const extendedTestClass = ExtendedEnum.from(testClass, {
foo: {
title: "Title for Foo",
description: "A simple description for entry foo..."
},
anotherFooBar: {
description: "Title here falls back to the key which is: anotherFooBar."
}
})
Advantages
No refactor needed, keep the basic enum structures as is.
You get code suggestions for each enum entry (vs code).
You get an error if you have a typo in declaring the enum descriptors aka associated types.
Get the benefit of associated types and stay on top of OOP paradigms.
It is optional to extend an enum entry, if you don't, this class generates the default entry for it.
Disadvantages
This class does not support enums with numerical keys (which shouldn't be annoying, because we usually use enums for human readability and we barely use numbers for enum keys)
When you assign a numerical value to a key; Typescript double-binds the enum key-values. To prevent duplicate entries, this class only considers string keys.

Resources