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 map
ping 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