3

I have a array of string with countries iso codes:

const countries = ['AF', 'AL', 'DZ', 'AS', 'AD', 'AO', 'AI', 'AQ', 'AG', ...]

And I would like to create a string enum in which the left and right side are the same, the values of the array, such as:

enum Country {
  AF = 'AF',
  AL = 'AL',
  DZ = 'DZ',
  AS = 'AS',
  AD = 'AD',
  AO = 'AO',
  AI = 'AI',
  AQ = 'AQ',
  AG = 'AG',
...
}

I have no idea how to do it. Thanks in advance.

biorubenfs
  • 643
  • 6
  • 15
  • Thanks @jcal. I needed a enum to use with zod library, but reading the documentation I realized that zod has a `z.enum()` that i can use with an array as I have (instead of use `z.nativeEnum()` method. But I would really appreciate a more detailed explanation about your answer. – biorubenfs Aug 09 '23 at 16:35
  • It's true that both answers are very similar and helpful, but yours include a more like-enum object, in the sense in which you can use dot notation to access every allowed value, like in a real enum. Thanks four your time and patience. – biorubenfs Aug 09 '23 at 17:23

3 Answers3

2

If the countries array is static, you can use the following to access the values as a string union type. While it does not behave like an enum at runtime (TypeScript enum is compiled down to a JavaScript object, while the union type is stripped by the compiler), it provides a typing at development/compile time.

const countries = ['AF', 'AL', 'DZ', 'AS', 'AD', 'AO', 'AI', 'AQ', 'AG'] as const;

type Country = (typeof countries)[number];

const testOk: Country = "AD";

See this TypeScript Playground.

Anon
  • 458
  • 4
  • 7
1

To create a string enum in TypeScript with the values from the countries array, you can use a combination of the array and a loop to generate the enum definition dynamically.

const countries = ['AF', 'AL', 'DZ', 'AS', 'AD', 'AO', 'AI', 'AQ', 'AG', /*...and so on*/];

enum Country {
  // This part will be generated dynamically based on the countries array
}

countries.forEach(countryCode => {
  Country[countryCode] = countryCode;
});

// Example usage
function getCountryName(country: Country) {
  switch (country) {
    case Country.AF:
      return 'Afghanistan';
    case Country.AL:
      return 'Albania';
    case Country.DZ:
      return 'Algeria';
    // Add more cases for other countries as needed
    default:
      return 'Unknown';
  }
}

const myCountry: Country = Country.AF;
console.log(getCountryName(myCountry)); // Output: Afghanistan
Kevin_H
  • 21
  • 2
  • Have you tried this in an IDE? I see [quite a few errors](https://tsplay.dev/w6zYyw) with this code. – jcalz Aug 01 '23 at 14:59
  • What error do you get? – Kevin_H Aug 02 '23 at 15:05
  • I linked above to the TypeScript Playground, a web IDE. If you are able to look at that it will show you exactly the errors I mean. If you are not able to look at that, let me know. – jcalz Aug 02 '23 at 15:06
1

TypeScript enums can't be produced programmatically from other TypeScript code (without using some sort of source code generation tool, anyway). But you can emulate much of the functionality of enums using plain objects with string/number literal types, as described in the TypeScript documentation.

So if you start with an array literal and you want the compiler to keep track of the specific literal string types of the elements, then you should use a const assertion to ask for that, otherwise the compiler will infer string[], allowing you to add/remove/reorder elements. You don't want to add/remove/reorder elements, so as const says that you're treating the array literal as if it were immutable, and thus it will always have a known length, and known elements, in a known order:

const countries = ['AF', 'AL', 'DZ', 'AS', 'AD', 'AO', 
  'AI', 'AQ', 'AG', /*...and so on*/] as const;

// const countries: readonly ["AF", "AL", "DZ", "AS", "AD", "AO", 
//   "AI", "AQ", "AG" , /*...and so on*/]

Now you have enough information to proceed.


Enums in TypeScript add both a named type and a named value into existence.

The type is the type of the enum values, meaning it's a union of all the possible enum value types. So let's create that by querying typeof countries (using the typeof type query operator to get its type) and indexing into that type with number, since if you have an array and index into it with some numeric key, you'll get some element:

type Country = (typeof countries)[number];
// type Country = "AF" | "AL" | "DZ" | "AS" | "AD" | "AO" | 
// "AI" | "AQ" | "AG" /*...and so on*/

The value is the object into which you actually index to get with keys to get the enum values. In your case you want the keys and values to be the same, so we can take the countries array to produce such an object, and then describe its type in the type system:

const Country = Object.fromEntries(
  countries.map(v => [v, v])
) as { [K in Country]: K };

This is using the Object.fromEntries() method to assemble an object from an array of key-value pairs, which I got by just mapping each element of countries to a pair where each element appears twice.

As for the type, that's a mapped type over the Country type (which is the union of intended enum values) as the keys, and for each key K in that, we want the value to be the same type, K.

Note that neither map() nor Object.fromEntries() are strongly typed enough for the TypeScript compiler to infer that the value produced is the right shape, so I've used a type assertion to say that that value can be treated as that type.

Let's look at both the value and the type:

/* const Country: {
    AF: "AF"; AL: "AL"; DZ: "DZ"; AS: "AS";
    AD: "AD"; AO: "AO"; AI: "AI"; AQ: "AQ";
    AG: "AG"; ...and so on
} */
console.log(Country)
/* {
  "AF": "AF", "AL": "AL", "DZ": "DZ", "AS": "AS",
  "AD": "AD", "AO": "AO", "AI": "AI", "AQ": "AQ",
  "AG": "AG"  ...and so on
}  */

Looks good!

And now you can use Country more or less as if it were a TypeScript enum:

console.log(Country.AG) // "AG"
const c: Country = Country.AL;

Do note that this isn't identical to an enum.

Enums actually bring a whole namespace into existence in which each key of the enum is present as a type exported from it, so that, for example, Country.AL is also a type referring to the type of the Country.AL value. You can simulate that if you want but not programmatically:

namespace Country {
    export type AF = typeof Country.AF;
    export type AL = typeof Country.AL;
    export type DZ = typeof Country.DZ;
    export type AS = typeof Country.AS;
    export type AD = typeof Country.AD;
    export type AO = typeof Country.AO;
    export type AI = typeof Country.AI;
    export type AQ = typeof Country.AQ;
    export type AG = typeof Country.AG;
    /* ...and so on */
}

so you could write

const d: Country.AL = Country.AL;

but I doubt that's worth it.

Furthermore enum value types are considered to be more specific than their literal equivalents. With our simulated enum we can write

const e: Country = "AI"; // okay 

but if you had a real enum then that would be an error, since "AI" is not considered assignable to Country.AI, even though they are have the same value at runtime. This restriction would prevent someone from accidentally passing a value from a different enum, like

enum Artist {
    Picasso = "Picasso",
    DaVinci = "Da Vinci",
    Robot = "AI"
}
const f: Country = Artist.Robot; // okay

which is acceptable in our case since Artist.Robot is assignable to "AI", which is a valid Country, but would be an error if Country were a enum.

These differences are minor, and for a lot of use cases the non-enum version is actually preferable. But you should be aware of them anyway.

Playground link to code

jcalz
  • 264,269
  • 27
  • 359
  • 360