Reputation: 14474
I want to narrow a string to a string literal union. In other words, I want to check if the string is one of the possible values of my literal union, so that this will work (if the operator couldbe
existed).
type lit = "A" | "B" | "C";
let uni: lit;
let str = "B";
if(str couldbe lit){
uni = str;
} else {
doSomething(str);
}
How can I achieve this?
I tried using if (str instanceof lit)
, but that doesn't seem to work. Using keyof
to iterate over the string union doesn't work either, because the allowed values aren't keys per se.
One way would be to use switch
with one case for each possible value, but that could lead to subtle errors if lit
s allowed values change.
Upvotes: 30
Views: 18448
Reputation: 8377
If you hate switch cases, as I do:
since TypeScript 3.4 – const assertions it's also possible to produce union type from array of your strings ^_^
const lits = <const>["A", "B", "C"];
type Lit = typeof lits[number]; // "A" | "B" | "C"
function isLit(str: string): str is Lit {
return lits.some((lit) => str === lit);
}
Upvotes: 24
Reputation: 55
You can do it w/o any casting and "is":
let litArr = ['A', 'B', 'C'] as const;
type lit = typeof litArr[number];
let uni: lit;
let str = 'B';
let foundStr = litArr.find(item => item === str);
if (foundStr) {
uni = foundStr;
} else {
doSomething(str);
}
Upvotes: 0
Reputation: 1492
You can also use a zod enum to do this:
import zod from 'zod'
const ColorMode = zod.enum(['light', 'dark', 'system'] as const)
let _mode = 'light' // type is string
let mode = ColorMode.parse(_mode) // type is "light" | "dark" | "system"
_mode = 'twilight'
mode = ColorMode.parse(_mode) // throws an error, not a valid value
You can also extract the type from the zod schema when needed:
type ColorMode = zod.infer<typeof ColorMode>
I find a validation library like this is the easiest and most robust way to parse, validate, and type-narrow variables/data when I would otherwise have to reach for manually-written and error-prone type guards/predicates.
Upvotes: 3
Reputation: 1572
This is my take on the problem with the type guard and with strictNullChecks
turned off (this is limitation on a project; if this option is true
TS will require exhaustiveness on the switch/case
).
Line const _notLit: never = maybeLit;
guaranties that when you change lit
type you need to update the switch/case
also.
Downside of this solution is that it gets very verbose as the union type lit
grows.
type lit = "A" | "B" | "C";
function isLit(str: string): str is lit {
const maybeLit = str as lit;
switch (maybeLit) {
case "A":
case "B":
case "C":
return true;
}
// assure exhaustiveness of the switch/case
const _notLit: never = maybeLit;
return false;
}
If possible this task is more suitable for enum
or if you require a type
and don't mind creating underlying enum for checking, you can create type guard something like this:
enum litEnum {
"A",
"B",
"C",
}
type lit = keyof typeof litEnum;
function isLit(str: string): str is lit {
return litEnum[str] !== undefined;
}
Upvotes: 3
Reputation: 2012
You can use User-Defined Type Guards.
type lit = "A" | "B" | "C";
let uni: lit;
let str = "B";
function isLit(str: string): str is lit {
return str == "A" || str == "B" || str == "C";
}
function doSomething(str: string) {
}
if (isLit(str)) {
uni = str;
}
else {
doSomething(str);
}
ADD:
To avoid duplicated edit, class
can be used both for compile-time and run-time. Now all you have to do is to edit just one place.
class Lit {
constructor(public A = 0, public B = 0, public C = 0) {}
}
type lit = keyof Lit;
let uni: lit;
function isLit(str: string): str is lit {
let lit = new Lit();
return (str in lit) ? true : false;
}
Upvotes: 9