Make enums more intuitive to use with familiar Set
and Map
interfaces! Works
with numeric, string, and heterogeneous enums, and allows for easy enum type
guarding, creation of enum subsets, and safe mapping of enums to anything
else—even other enums!
Package: GitHub, npm | Releases: Changelog | Author: Shaun Grady
npm install @sg.js/enum-utils
Requires TypeScript >=4.7
EnumSet
: Use an enum as an immutable Set.
- Construction via
fromEnum(Enum)
method takes a single enum argument. has(value)
is a type guard for narrowing serialized values types to the enum.subset(Enum[])
safely creates anEnumSet
from a subset of enum members.toEnumMap(mapping)
creates anEnumMap
, safely mapping an enum (or enum subset) to anything.- Shares
Set
iterable methods.
EnumMap
: Use an enum as an immutable Map.
- Construction via
fromEnum(Enum, { [Enum]: any })
provides exhaustive type safety for map keys while preventing numeric enum keys from being coerced to strings. has(value)
is a type guard for narrowing serialized values types to the enum.get()
has dynamically-typed return value;has()
guarded values always return a value and illegal enum values always returnundefined
.- Shares
Map
iterable methods.
enumToSet
: Convert an enum object to Set of enum members.isEnumMember
: Type guard for a given enum.isValidEnumMember
: Type guard for values that are strings and finite numbers.
EnumSetMembers<EnumSet>
: Returns either the original enum, or a union type of enum members for enum subsets.EnumMapMembers<EnumMap>
: Returns either the original enum, or a union type of enum members for enum subsets.EnumMapValues<EnumMap>
: Returns a union type of the EnumMap's values; works best when the map object includes a const assertion.EnumMember
: A finite number or string.
enum Priority {
Low = 'L',
Medium = 'M',
High = 'H',
ThisIsFine = 'OhNo',
}
// Define our base EnumSet with all Priority members
const priorities = EnumSet.fromEnum(Priority);
// EnumSets are immutable, so aliasing to another variable is safe.
const adminPriorities = priorities;
// Non-admins will only be allowed to use a subset of priorities.
const userPriorities = adminPriorities.subset([
Priority.Low,
Priority.Medium,
Priority.High,
]);
// Create a map with values constrained to a union of i18n keys.
priorityI18nMap = priorities.toEnumMap<I18nKey>({
[Priority.Low]: 'common.low',
[Priority.Medium]: 'common.medium',
[Priority.High]: 'common.high',
[Priority.ThisIsFine]: 'common.makeItStop',
});
// EnumMaps can also be constructed like this:
// EnumMap.fromEnum(Priority, { … } as const)
// However, value types aren't as easily constrained with a single type argument,
// so using the `as const` assertion for the mapping is recommended
// for maximum type safety.
const PrioritySelect = () => {
const { t } = useTranslation();
// Determine which Priority to set based on user's role
const { isAdmin } = useSession();
const allowedPriorities = isAdmin ? adminPriorities : userPriorities;
// This component allows a `priorityLock` search param to be set that disables
// the priority select with the given priority. Very secure. 👌
const { query } = useRouter();
const priorityLock: unknown = query.priorityLock;
const hasPriorityLock = priorityLock != null;
// Guard `priorityLock` to our Priority enum type
if (hasPriorityLock && !allowedPriorities.has(priorityLock)) {
const priorityList = [...allowedPriorities].join(', ');
throw new Error(
`searchParam 'lockPriority' must be one of: ${priorityList}.`,
);
}
const [priority, setPriority] = useState<Priority>(
priorityLock ?? allowedPriorities.values().next().value,
);
return (
<Select
onchange={setPriority}
disabled={hasPriorityLock}
optionValues={Array.from(allowedPriorities)}
renderOption={(priority: Priority) => t(priorityI18nMap.get(priority))}
/>
);
};
Although new EnumSet()
can be called with the same value signature of Set
,
the type arguments aren't very developer-friendly; instead, it's recommended to
make use of the EnumSet.fromEnum()
static method.
Creates a new EnumSet
instance from the given enum object.
fromEnum(Enum);
enum Color {
Red,
Green,
Blue,
}
const colors = EnumSet.fromEnum(Color);
The has() method returns a boolean indicating whether an enum member with
the specified value exists in the EnumSet
object or not, acting as a
type guard.
has(value);
const colors = EnumSet.fromEnum(Color);
const value: unknown = router.query.color;
let color: Color;
if (colorToHexMap.has(value)) {
color = value;
}
The subset() method returns a new EnumSet
instance containing only the
enum members specified, which must be members of the EnumSet
.
subset([Enum]);
enum Locale {
enUS = 'en-US',
enGB = 'en-GB',
frCA = 'fr-CA',
esMX = 'es-MX',
jaJP = 'ja-JP',
}
const siteLocales = EnumSet.from(Locale);
const videoLocales = siteLocales.subset([
Locale.enUS,
Locale.enGB,
Locale.esMX,
]);
if (videoLocales.has(value)) {
// typeof value ⮕ `Locale.enUS | Locale.enGB | Locale.esMX`
}
Returns an EnumMap
instance that maps each enum member in the EnumSet
to a
corresponding value in the given mappings object. The mapping object value types
may either be inferred or defined by the optional type argument. If inferred,
it's recommended to use the as const
assertion on the mapping object to narrow
the value types.
toEnumMap(mapping);
Given the following locales
EnumSet
instance…
enum Locale {
enUS = 'en-US',
enGB = 'en-GB',
frCA = 'fr-CA',
}
const locales = EnumSet.from(Locale);
An EnumMap
can be created with string
type values thusly:
const localeFileSuffixes = locales.toEnumMap({
[Locale.enUS]: 'en',
[Locale.enGB]: 'en',
[Locale.frCA]: 'fr-ca',
});
const value: string = localeFileSuffixes.get(Locale.enUS);
However, there are a few ways to increase the type safety of your map values. We can define the map's value type by passing it as the first type argument of the method:
const localeI18nKeys = locales.toEnumMap<I18nKeys>({
[Locale.enUS]: 'common.americanEnglish',
[Locale.enGB]: 'common.britishEnglish',
[Locale.frCA]: 'common.canadianFrench',
});
const i18nValue: I18nKeys = localeFileSuffixes.get(Locale.enUS);
The value type for the map can be narrowed further with the as const
assertion:
const localeI18nKeys = locales.toEnumMap({
[Locale.enUS]: 'common.americanEnglish',
[Locale.enGB]: 'common.britishEnglish',
[Locale.frCA]: 'common.canadianFrench',
} as const);
const localeValues:
| 'common.americanEnglish'
| 'common.britishEnglish'
| 'common.canadianFrench' = localeFileSuffixes.get(Locale.enUS);
However, the above example isn't protecting against strings that don't exist in
our I18nKeys
union type. For the greatest type safety, the as const
assertion can be paired with the satisfies
operator:
const localeI18nKeys = locales.toEnumMap({
[Locale.enUS]: 'common.americanEnglish',
[Locale.enGB]: 'common.britishEnglish',
// @ts-expect-error
[Locale.frCA]: 'foo.bar',
} as const satisfies Record<Locale, I18nKeys>);
This approach combines the type narrowing of as const
with the type checking
of satisfies
. It ensures that all keys of the Locale enum are present and that
all values are valid I18nKeys. This provides the strongest type safety, catching
errors at compile time.
These methods behave identically to the Set
class.
See MDN documentation
for more details.
Although new EnumMap()
can be called with the same value signature of Map
,
the type arguments aren't very developer-friendly; instead, it's recommended to
make use of the EnumMap.fromEnum()
static method, or the
EnumSet.prototype.toEnumMap()
instance method, which allows for typing the
mapping keys more easily.
Returns an EnumMap
instance that maps each enum member in the given enum to a
corresponding value in the given mappings object. The mapping object value types
may either be inferred or defined by the optional type argument. If inferred,
it's recommended to use the as const
assertion on the mapping object to narrow
the value types.
See EnumSet.prototype.toEnumMap()
for nuances
related to type narrowing and safety with the mapping object.
fromEnum(Enum, mapping as const);
enum Color {
Red,
Green,
Blue,
}
const colorHexMap = EnumMap.fromEnum(Color, {
[Color.Red]: '#f00',
[Color.Green]: '#0f0',
[Color.Blue]: '#00f',
} as const);
The has() method returns a boolean indicating whether an enum member with
the specified value exists in the EnumMap
object or not, acting as a
type guard.
has(value);
const colorToHexMap = EnumMap.fromEnum(Color, {
[Color.Red]: '#f00',
[Color.Green]: '#0f0',
[Color.Blue]: '#00f',
} as const);
const value: unknown = router.query.color;
let color: Color;
if (colorToHexMap.has(value)) {
color = value;
}
The get() method returns a specified element from an EnumMap
object. If
the key's value has been guarded by the has() method, then the return type
will be non-nullish. If the key is an illegal enum member type, then return type
will be undefined
.
get(value);
const colorToHexMap = EnumMap.fromEnum(Color, {
[Color.Red]: '#f00',
[Color.Green]: '#0f0',
[Color.Blue]: '#00f',
} as const);
const value: unknown = router.query.color;
let colorHex: '#f00' | '#0f0' | '#00f';
if (colorToHexMap.has(value)) {
colorHex = colorToHexMap.get(value);
}
These methods behave identically to the Map
class.
See MDN documentation
for more details.
Converts an enum runtime object to an array of its members. This is safe to use with numeric, string, and heterogeneous enums.
enum Fruit {
Apple = 'apple',
Banana = 'banana',
Orange = 'orange',
}
const fruitSet: EnumSet<Fruit> = enumToSet<Fruit>(Fruit);
console.log(fruitSet); // ⮕ Set { 'apple', 'banana', 'orange' }
Checks if the given value is a valid member of the specified enum object, acting as a type guard.
enum CatBreed {
Siamese,
NorwegianForestCat,
DomesticShorthair,
}
if (isEnumMember(CatBreed, 'Greyhound')) {
// …
}
Type guards values as an eligible enum member: a finite number or string.
isValidEnumMember('foo'); // ⮕ true
isValidEnumMember(42); // ⮕ true
isValidEnumMember(NaN); // ⮕ false
isValidEnumMember({}); // ⮕ false