I have a string enumeration that looks like:
export enum FMEvents {
RECORD_ADDED = "@firemodel/RECORD_ADDED",
RECORD_CHANGED = "@firemodel/RECORD_CHANGED",
RECORD_MOVED = "@firemodel/RECORD_MOVED",
RECORD_REMOVED = "@firemodel/RECORD_REMOVED",
}
I'd like to be able to be able to constrain input into a function to the string values of enumeration (e.g., "@firemodel/RECORD_ADDED", etc.).
I thought I could maybe just do the following for the method signture:
public doSomething(event: keyof FMEvents) { ... }
but the typing for that is all wrong (I think it's giving me the keys of the enum object, not sure but definitely wrong).
I then tried just:
public doSomething(event: FMEvents) { ... }
This allows me to call doSomething()
with FMEvents.RECORD_CHANGED
but it does not allow me to call it with the resolved key of doSomething("@firemodel/RECORD_CHANGED")
.
What I'm looking for is a way to constraint it to the string defined as values in the Enum and nothing else. With this I'm hoping both calling methods above will pass the type checking.
TypeScript 4.1 introduced template literal types which, among other things, will convert enums to their string representations. So your goal can be accomplished simply as:
function doSomething(event: `${FMEvents}`) { }
doSomething("@firemodel/RECORD_CHANGED"); // okay
doSomething(FMEvents.RECORD_MOVED); // still okay
Playground link to code
Pre TS4.1 answer:
TypeScript doesn't make it easy to widen enum value types to the string or numeric literals from which they derive. (There is a complication that prevents using intersections to help with this) You can get fairly close to what you want using conditional types:
type Extractable<T, U> = T extends U ? any : never
type NotString<T> = string extends T ? never : any
function promoteStringToFMEvents<K
extends string & NotString<K> & Extractable<FMEvents, K>>(
k: K
): Extract<FMEvents, K> {
return k;
}
const fmAdded = promoteStringToFMEvents("@firemodel/RECORD_ADDED"); // FMEvents.RECORD_ADDED
const fmOops = promoteStringToFMEvents("@firemodel/RECORD_ADDLED"); // error
In the above code, Extractable<T, U>
returns any
if T
or any of its constituents is assignable to U
, and never
otherwise. And NotString<T>
returns any
is T
isn't string
or wider, and never
otherwise. By constraining K
in promoteStringToFMEvents()
to string & NotString<K> & Extractable<FMEvents, K>
, we are saying that the type parameter K
must be some string literal (or union of string literals) that some element (or union of elements) of FMEvents
can be assigned to.
So the function promoteStringToFMEvents()
will accept the string literals (or unions of string literals) you expect. The function also just returns the associated element of FMEvents
by assigning the input value to Extract<FMEvents, K>
, which pulls out just those pieces of FMEvents
which match K
.
So you can write your doSomething()
method such that it is generic in the type of K
above, and in the implementation of the method you can (if you need to) promote the string to an enum by assigning it to a variable of type Extract<FMEvents, K>
.
EDIT with explicit implementation of doSomething()
:
class Blomp {
public doSomething<K
extends string & NotString<K> & Extractable<FMEvents, K>>(k: K) {
// k is of some subtype of "@firemodel/RECORD_ADDED" |
// "@firemodel/RECORD_CHANGED" | "@firemodel/RECORD_MOVED" |
// "@firemodel/RECORD_REMOVED"
// if you need to interpret k as a subtype of FMEvents, you can:
const kAsFMEvent: Extract<FMEvents, K> = k;
// or even wider as just FMEvents
const fmEvent: FMEvents = kAsFMEvent;
// do what you want here
}
}
Hope that helps. Good luck!
The list of values of an enum can be infered as a type with some help of the template literal operator:
export enum FMEvents {
RECORD_ADDED = "@firemodel/RECORD_ADDED",
RECORD_CHANGED = "@firemodel/RECORD_CHANGED",
RECORD_MOVED = "@firemodel/RECORD_MOVED",
RECORD_REMOVED = "@firemodel/RECORD_REMOVED",
}
type FMEventsValue = `${FMEvents}`
// => type FMEventsValue = "@firemodel/RECORD_ADDED" | "@firemodel/RECORD_CHANGED" | ...
const event: FMEventsValue = "@firemodel/RECORD_ADDED"
// => ✅ OK
const event: FMEventsValue = "NoT_iN_ThE_eNuM"
// => 🚨 KO
Reference article: Get the values of an enum dynamically (disclaimer: author here)
If you love us? You can donate to us via Paypal or buy me a coffee so we can maintain and grow! Thank you!
Donate Us With