24

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.

iFreilicht
  • 13,271
  • 9
  • 43
  • 74
  • 1
    The type `lit` doesn't exist at runtime so you cannot use it like that. Maybe use an enum instead? – Nitzan Tomer May 16 '17 at 14:54
  • Regarding the switch statement comment, see this [answer](https://stackoverflow.com/questions/39419170/how-do-i-check-that-a-switch-block-is-exhaustive-in-typescript). – David Sherret May 16 '17 at 14:55
  • @NitzanTomer That is actually a very good idea, seems much cleaner and easier to understand. – iFreilicht May 16 '17 at 19:05

4 Answers4

14

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);
}
damd
  • 6,116
  • 7
  • 48
  • 77
am0wa
  • 7,724
  • 3
  • 38
  • 33
6

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;
}
thatseeyou
  • 1,822
  • 13
  • 10
  • Hm, that's a little better than switch statements, but it still has the problem of possibly forgetting to update the type guard when `lit`s permitted values are changed. – iFreilicht May 16 '17 at 16:46
  • I added another solution to my answer. – thatseeyou May 16 '17 at 17:51
  • 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 `enum`s, though. – iFreilicht May 16 '17 at 19:07
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;
}
kaznovac
  • 1,303
  • 15
  • 23
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.

Roman Scher
  • 1,162
  • 2
  • 14
  • 18