2

I receive a Typescript error that string is not of type 'food' | 'drink' | other, when I do:

order = x

Because Typescript assumes that x can be any string, and not just 'food' | 'drink' | other, so it throws an error.

Of course, I can get rid of this error if I put my code inside the type guard like this:

const order = 'food' // default value
if (x === 'food' || x === 'drink' || x === 'other') {
 order = x
}

I just wonder, can I make this code shorter, DRY and non-repetitive?

Just to let you know, putting the strings in an array ['food', 'drink', 'other'] and doing the condition if (array.includes(x)) {order = x}, doesn't do the trick!

5
  • What does the declaration or assignment of x look like? Commented Jan 31, 2022 at 20:19
  • x is the string value I get from the API. Commented Jan 31, 2022 at 20:20
  • is x a fixed set of strings? otherwise, there is no way typescript can infer it. Commented Jan 31, 2022 at 20:22
  • Yes, API can return only this 3 values for x - 'food', 'drink', 'other'. Commented Jan 31, 2022 at 20:26
  • Do you have a type declaration for the results from the API? Commented Jan 31, 2022 at 20:28

1 Answer 1

3

You could use a type predicate function to cast this to the correct type:

function isMember<T extends string>(
  array: readonly T[],
  value: string
): value is T {
  return (array as readonly string[]).includes(value)
}

This accepts an array, and a value. The member type of the array is captured as the generic parameter T. The return value is value is T, which means that if true is returned, than the variable passed as value will be considered a T, and if false is returned then it will be treated as the type of the argument string.

Which you could use like so:

const x: string = 'drink'
let order: 'food' | 'drink' | 'other' = 'food' // default value

if (isMember(['food', 'drink', 'other'], x)) {
    order = x // x is of type: 'food' | 'drink' | 'other'
}

Playground


Now to clean this up a bit, you can setup your type and constants like so:

const yummyStrings = ['food', 'drink', 'other'] as const
type YummyString = (typeof yummyStrings)[number] // 'food' | 'drink' | 'other'

And use those like this:

const x: string = 'drink'
let order: YummyString = 'food' // default value

if (isMember(yummyStrings, x)) {
    order = x // x is of type: 'food' | 'drink' | 'other'
}

Playground

Sign up to request clarification or add additional context in comments.

8 Comments

Yeah I was looking at this code. includes() doesn't already behave this way for the reasons outlined in github.com/microsoft/TypeScript/issues/26255 and github.com/microsoft/TypeScript/issues/36275.
I do not doubt that this solution works, but it is more complicated than simple: if (x === 'food' || x === 'drink' || x === 'other') { order = x }
Properly type narrowing from untyped data sources like parsed JSON is not a simple task, sadly. But the "right" solution here depends on how many places you want to do this, and how many possible members there are, and how often that list will be updated. If you answered "a lot" to any of those, than a robust "complicated" solution will probably serve you best.
An analogy, possibly... Person A: Is there some simpler, less repetitive way to add these numbers together? let s = 8 + 6 + 7 + 5 + 3 + 0 + 9; Person B: you could write let s = [8, 6, 7, 5, 3, 0, 9].reduce((a, x) => x + a); Person A: well, that's less repetitive but it's longer and more complicated than the simple version I started with. Person B: 🤷‍♂️
The real point, I suppose, is that isMember(yummyStrings, x) is "shorter, DRY and non-repetitive" when compared to x === 'food' || x === 'drink' || x === 'other'
|

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service and acknowledge you have read our privacy policy.

Start asking to get answers

Find the answer to your question by asking.

Ask question

Explore related questions

See similar questions with these tags.