Skip to main content

Iterable union types (enums)

Named iterable union types (NIUTs) are a powerful alternative to enums that bring strong typing while avoid caveats & quirks of TS enums.

Defining a NIUT

When you define your object, try to follow some ground rules:

  • In your definition, name both the object and the TS type as singular (i.e. CHOICE/Choice and not CHOICES/Choices).
  • In your object definition, the key and value should be identical and both all-caps.
  • When iterating over the object definition, use Object.values (and not Object.keys) for best TS support.
// Define the "enum" as an object
// IMPORTANT: you must include `as const`
const CHOICE = {
APPLE: "APPLE",
BLUEBERRY: "BLUEBERRY",
CABBAGE: "CABBAGE",
} as const;

// Define the TS union type
// IMPORTANT: This is the only "magic" line; just copy/paste and edit
// It's best to name this singular (i.e. Choice and not Choices)
type Choice = (typeof CHOICE)[keyof typeof CHOICE]; // 'a' | 'b' | 'c'

Using your NIUT

// You can use the value directly
const myChoice: Choice = CHOICE.APPLE; // OK
const notMyChoice: Choice = "d"; // TypeError

// To iterate over this enum, iterate its values (and not its keys)
Object.values(CHOICES).map((choice) => {
logger.info(choice);
});

Unnamed iterable union types (IUTs)

Deprecated

Please don't define any new unnamed IUTs, and use named IUTs instead!

Unnamed IUTs are used everywhere in our codebase. They are similar to NIUTs, except that they are defined as arrays (not objects).

Unnamed IUTs have a simpler construction that allows them to be directly iterated over. In exchange for this simplicity, they are referenced as strings ("APPLE", versus CHOICES.APPLE). This makes it harder to disambiguate between a magic string and a typed string. Thus, we are starting to phase this approach out in favor of NIUTs.

// Define the "enum" as an array
// IMPORTANT: include `as const`
// It's best to name this plural (i.e. CHOICES and not CHOICE)
const CHOICES = ["APPLE", "BLUEBERRY", "CABBAGE"] as const;

// Define the TS union type
// This is the only "magic" line; just copy/paste and edit
// It's best to name this singular (i.e. Choice and not Choices)
type Choice = (typeof CHOICES)[number]; // 'APPLE' | 'BLUEBERRY' | 'CABBAGE'

// You can use the value directly
const myChoice: Choice = "a"; // OK
const notMyChoice: Choice = "d"; // TypeError

// You can iterate over the enum as you expect
CHOICES.map((choice) => {
logger.info(choice);
});