Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

getting a type for the values of a string enum

Tags:

typescript

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.

like image 454
ken Avatar asked Mar 07 '23 00:03

ken


2 Answers

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!

like image 109
jcalz Avatar answered Mar 24 '23 10:03

jcalz


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)

like image 32
Arnaud Leymet Avatar answered Mar 24 '23 11:03

Arnaud Leymet