13

I have an array of given union type, then wants to check if a string from a superset of the union type is contained in the array (runtime check):

const validOptions: ("foo" | "bar")[] = ["foo", "bar"]
type IArrType = typeof validOptions[number]
const key: IArrType | "alien" = "alien" // Rather: some random function
const isKeyInArr = validOptions.indexOf(key) > -1 // Error: "alien" is not assignable to "foo" | "bar"

// Fix 1:
const isKeyValidCast = validOptions.indexOf(<IArrType>key) > -1 
// Fix 2:
const isKeyValidExplicit = 
      key === "alien" ? false : validOptions.indexOf(key) > -1 // OK: type guard magic

Fix 1 is OK but not very elegant. Fix 2 fools the compiler but is misleading and inefficient runtime. In my case the "alien" string type is just a placeholder for any string not in the union type.

Is there any ways this can be compiled without casting or explicit tests? Can the expression be negated so that we get this "type guard" to work?

BTW: This very cool answer show how to construct a typed tuple from a list of values: Typescript derive union type from tuple/array values

djechlin
  • 59,258
  • 35
  • 162
  • 290
Jørgen Tvedt
  • 1,214
  • 3
  • 12
  • 23
  • 1
    Probably `(arr as string[]).indexOf(key)` is your best bet. – jcalz Apr 29 '18 at 23:44
  • @jcalz - Yes, I agree that casting the arrary is better than casting the argument. It's more clear to the intention of the code. Ended up using a custom guard function and `find()`, however. – Jørgen Tvedt Apr 30 '18 at 10:51

2 Answers2

16

The accepted answer uses type assertions/casting but from the comments it appears the OP went with a solution using find that works differently. I prefer that solution also, so here's how that can work:

const configKeys = ['foo', 'bar'] as const;
type ConfigKey = typeof configKeys[number]; // "foo" | "bar"

// Return a typed ConfigKey from a string read at runtime (or throw if invalid).
function getTypedConfigKey(maybeConfigKey: string): ConfigKey {
    const configKey = configKeys.find((validKey) => validKey === maybeConfigKey);
    if (configKey) {
        return configKey;
    }
    throw new Error(`String "${maybeConfigKey}" is not a valid config key.`);
}

Note that this can guarantee that a string is a valid ConfigKey both at runtime and compile time.

jtschoonhoven
  • 1,948
  • 1
  • 19
  • 16
11

The biggest problem is how to handle all possible values that are not ConfigurationKeys without explicitly checking each one. I named them Configuration as it's very common scenario.

You can hide logic behind your own guard function that tells compiler: I can handle type checks, trust me. It's recognized by value is ConfigurationKeys return type.

Code example (live):

type ConfigurationKeys = "foo" | "bar";

function isConfiguration(value: string): value is ConfigurationKeys {
    const allowedKeys: string[] = ["foo", "bar"];
    
    return allowedKeys.indexOf(value) !== -1;
}

const key: string = "alien" // Rather: some random function

if (isConfiguration(key)) { 
    // key => ConfigurationKeys
} else { 
    // key => string
}

I found writing own guard functions as very clean solution to work with Union types. Sometimes type casting is still needed, but here you hide casting and logic within single piece of code.

Reference:

BarelyFitz
  • 1,967
  • 13
  • 17
Piotr Lewandowski
  • 6,540
  • 2
  • 27
  • 32
  • I added a custom guard function and it does improve the code quality a little. However I still need the cast, as my version of the `allowedKeys` structure is a tuple of typed strings. Hence the `indexOf` expression will not compile without it. – Jørgen Tvedt Apr 30 '18 at 06:09
  • It's fine to do type casting *within* guard function. That's the place where you check things possible in runtime but unforeseen in compile-time. Even in official doc their do so (see reference). – Piotr Lewandowski Apr 30 '18 at 08:32
  • 2
    You could also use `find` which has different type definition that allows values different from base type. See: `allowedKeys.find(el => el === value) !== undefined` – Piotr Lewandowski Apr 30 '18 at 08:53
  • 1
    Using `find` together with a custom guard gets me close enough. The construct is easy to reason about, without any casts. Thank you! – Jørgen Tvedt Apr 30 '18 at 10:47
  • 1
    type ConfigurationKeys | string is collapsed to string by compiler, so you can simplify isConfiguration(value: ConfigurationKeys | string) to isConfiguration(value: string) – Nail Achmedzhanov Mar 14 '19 at 05:13
  • 6
    Type guards are great, but this is a dangerous pattern as currently written. If `ConfigurationKeys` ever changes and you forget to update `allowedKeys` to match, then you will have a type guard that is incorrectly validating strings at compile time *and* runtime. It's an easy change to type check `allowedKeys` against `ConfigurationKeys`. – jtschoonhoven Apr 09 '20 at 23:22