1

Is there a way to create a generic function in TypeScript to convert an enum to an array that also accepts null values?

Something like this:

enum Color
{
    RED   = "Red",
    GREEN = "Green",
    BLUE  = "Blue"
}

let colors = convertEnumToArray(Color);
colors.push(null);

Where colors is of type (Color | null)[].

I've found the function below in this link and I'd like to modify it to allow null in the array:

https://blog.oyam.dev/typescript-enum-values/

type EnumObject = {[key: string]: number | string};
type EnumObjectEnum<E extends EnumObject> = E extends {[key: string]: infer ET | string} ? ET : never;

function convertEnumToArray<E extends EnumObject>(enumObject: E): Array<EnumObjectEnum<E> | null> {
    const elements = Object.values(enumObject)
        .filter(key => Number.isNaN(Number(key)))
        .map(key => enumObject[key] as (EnumObjectEnum<E> | null));
    elements.unshift(null);
    return elements;
}

But I think there should be an easier way, or am I wrong?

Marcos
  • 1,237
  • 1
  • 15
  • 31
  • Just write `let colors: (Color | null)[] = convertEnumToArray(Color);`, even if the return type of `convertEnumToArray(Color)` is `Color[]` – Bergi Jun 14 '23 at 01:24
  • I would like that type returned from the function itself, not in the call site (which was, by the way, my first "solution"). – Marcos Jun 14 '23 at 01:32
  • If you're happy with the function from that blog, all you need to do is annotate the return type's array element type with `| null`, as shown [in this playground link](https://tsplay.dev/WYQgrW). That's the "easiest" way, given that function. But that function acts weird given a mixed numeric/string enum. If you want to write it yourself, you could do it like [this playground link](https://tsplay.dev/Nn826W). Which of those solutions would you like to see posted as an answer, if any? – jcalz Jun 14 '23 at 03:26
  • The function is ok to me as I don't mix types in enums. It seems that the function is already simple enough for the job and can't be made easier. First solution is better. – Marcos Jun 14 '23 at 03:44
  • Then could you please [edit] the question to specifically say: "I have this function and I'd like to modify it to allow `null` in the array?" That way the answer doesn't have to delve into the general "how to create this function" question, which would almost certainly involve concerns not addressed by the function from that blog, especially for future readers who may have slightly different use cases that match the current question wording. – jcalz Jun 14 '23 at 03:50
  • 1
    I've edited the question. – Marcos Jun 14 '23 at 10:20

1 Answers1

0

Arrays in TypeScript are considered to be covariant in their element types (see Difference between Variance, Covariance, Contravariance and Bivariance in TypeScript and Why are TypeScript arrays covariant?). So that means that if you have an Array<X> for some type X, then you can use it directly as an Array<X | null>, since the union type X | null is wider than X. So if you've got a function that returns X[] and you'd like it to return (X | null)[], all you need to do is annotate the return type as (X | null)[]. The compiler will accept it.

So starting with the function from that blog, which we assume meets your needs except for the null element part:

function getEnumValues<E extends EnumObject>(
    enumObject: E
): EnumObjectEnum<E>[] {
    return Object.keys(enumObject)
        .filter(key => Number.isNaN(Number(key)))
        .map(key => enumObject[key] as EnumObjectEnum<E>);
}

the only change necessary is in the annotated return type:

function getEnumValues<E extends EnumObject>(
    enumObject: E
): (EnumObjectEnum<E> | null)[] { // <-- change here
    return Object.keys(enumObject)
        .filter(key => Number.isNaN(Number(key)))
        .map(key => enumObject[key] as EnumObjectEnum<E>);
}

And then your code will work as desired:

enum Color {
    RED = "Red",
    GREEN = "Green",
    BLUE = "Blue"
}

let colors = getEnumValues(Color);
// let colors: (Color | null)[]
colors.push(null); // okay

That's it.

Again, for any future readers, it is just assumed that the original function works as desired; in practice, that definition might do weird things if you have mixed numeric/string enums. The point of this answer is merely to show how you can just add | null to the right place in a return type annotation, and not to endorse any particular function for converting an enum to an array of its values.

Playground link to code

jcalz
  • 264,269
  • 27
  • 359
  • 360