Hedge
Hedge

Reputation: 1231

Type guard for allowed enum value with dynamic string?

I'm working with some data from a 3rd party that I want to convert into to a map by id, but only if the data is valid. I have an enum of allowed properties, but can't figure out how to check that the data is valid in a way the compiler will allow. I've attempted to check using an if statement with the in operator on the enum:

/** Allowed action values */
enum Actions {
    Update = 'Update',
    Delete = 'Delete'
}

/** Validated app events */
type AppEvent = {
    id: string;
    action: Actions;
}

/** 3rd party data I want to validate */
type RawEvent = {
    id: string;
    // ❗️I want to make sure this string is an allowed enum
    action: string
}

type AppEventsById = {
    [id: string]: AppEvent
}

const possiblyInvalidEvents: RawEvent[] = [
    {
        id: 'typescript',
        action: 'Update'
    },
    {
        id: 'uh oh',
        action: 'oops'
    }
]

const eventsById: AppEventsById = {}

possiblyInvalidEvents.forEach(event => {
    // ❓Here I'm attempting to check that 3rd party action field is one of the allowed enums
    if (event.action in Actions) {
        eventsById[event.id] = {
            id: event.id,
            // 💥Type 'string' is not assignable to type 'Actions'
            action: event.action
        }
    }
})
// => I want eventsById to include the { id: 'typescript' } object, but not { id: 'uh oh' }

The attempted assignment to action throws this error: Type 'string' is not assignable to type 'Actions'.

Upvotes: 2

Views: 2536

Answers (2)

jcalz
jcalz

Reputation: 329773

You want a user-defined type guard function to check if a string is a valid Actions member. This is a way to explicitly tell the compiler that some boolean-valued expression should be used to narrow the type of a value if it turns out to be true. The simplest refactoring of your code would be this:

function isValidAction(str: string): str is Actions {
    return str in Actions;
}

possiblyInvalidEvents.forEach(event => {
    if (isValidAction(event.action)) {
        eventsById[event.id] = {
            id: event.id,
            action: event.action // no error anymore
        }
    }
})

This str in Actions check really relies on the fact that the enum's keys and values are identical, which might not always be true. I'd probably feel more comfortable checking the actual values of the enum and not the keys, which is a little more obnoxious to write out, but at least is less likely to suddenly break:

function isValidAction(str: string): str is Actions {
    return (Object.keys(Actions) as Array<keyof typeof Actions>).
        some(k => Actions[k] === str);
}

But it's up to you. Okay, hope that helps; good luck!

Playground link to code

Upvotes: 2

Alex Wayne
Alex Wayne

Reputation: 187222

You just need a refinement function that asserts that some value is Actions. Any code that that branches based on this return value will remember that your value is indeed the type that you have insisted it is. Because you know it is.

function isAction(input: string): input is Actions {
  return input in Actions
}

To use it simply call the function and branch if it returns true:

let someAction: Actions = Actions.Delete

const actionName: string = 'whatever'
if (isAction(actionName)) {
  // Legal here, since we have verified the type.
  someAction = actionName
}

Playground link

Upvotes: 2

Related Questions