Narrowing string to string literal union
Asked Answered
B

5

30

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 lits allowed values change.

Buckbuckaroo answered 16/5, 2017 at 14:43 Comment(3)
The type lit doesn't exist at runtime so you cannot use it like that. Maybe use an enum instead?Front
Regarding the switch statement comment, see this answer.Substantialize
@NitzanTomer That is actually a very good idea, seems much cleaner and easier to understand.Buckbuckaroo
D
22

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 list[number]; // "A" | "B" | "C"

function isLit(str: string): str is Lit {
  return !!lits.find((lit) => str === lit);
}
Domingo answered 29/3, 2021 at 18:53 Comment(0)
K
9

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;
}
Kaenel answered 16/5, 2017 at 15:48 Comment(3)
Hm, that's a little better than switch statements, but it still has the problem of possibly forgetting to update the type guard when lits permitted values are changed.Buckbuckaroo
I added another solution to my answer.Kaenel
I like your second solution very much if one absolutely had to use literal unions. It seems like a better way would be to just switch to enums, though.Buckbuckaroo
E
3

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;
}
Enterpriser answered 23/10, 2020 at 2:58 Comment(0)
K
3

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.

Kinnard answered 5/12, 2022 at 18:20 Comment(0)
G
0

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);
}
Gironde answered 17/1, 2024 at 8:16 Comment(0)

© 2022 - 2025 — McMap. All rights reserved.