Is it possible to verify the legitimacy of a union type of string literals during program execution

Looking for validation of a simple union type consisting of string literals to ensure its accuracy due to FFI calls to "regular" Javascript. Is there a method to confirm that a specific variable is an instance of any of these literal strings during runtime? Something similar to:

type MyStrings = "A" | "B" | "C";
MyStrings.isAssignable("A"); // true
MyStrings.isAssignable("D"); // false

Answer №1

At the current version of Typescript 3.8.3, there isn't a definitive best practice for this particular scenario. There are three potential solutions that do not rely on external libraries. In all cases, it is necessary to store the strings in an object that is accessible at runtime (such as an array).

For the purpose of illustration, let's say we require a function to validate at runtime if a given string matches any of the predefined sheep names, which are commonly known as Capn Frisky, Mr. Snugs, and Lambchop. Here are three approaches to accomplish this while ensuring compatibility with the Typescript compiler.

1: Type Assertion (Simpler Method)

Take off your helmet, manually verify the type, and apply an assertion.

const sheepNames = ['Capn Frisky', 'Mr. Snugs', 'Lambchop'] as const;
type SheepName = typeof sheepNames[number]; // "Capn Frisky" | "Mr. Snugs" | "Lambchop"

// The content of this string will be evaluated at runtime; TypeScript cannot determine its type validity.
const unsafeJson = '"Capn Frisky"';

/**
 * Retrieve a valid SheepName from a JSON-encoded string or throw an error.
 */
function parseSheepName(jsonString: string): SheepName {
    const maybeSheepName: unknown = JSON.parse(jsonString);
    
    if (typeof maybeSheepName === 'string' && sheepNames.includes(maybeSheepName)) {
        return (maybeSheepName as SheepName); // type assertion satisfies the compiler
    }
    
    throw new Error('The provided string does not match any of the sheep names.');
}

const definitelySheepName = parseSheepName(unsafeJson);

PRO: Simple and easy to comprehend.

CON: Fragile. You rely solely on your verification without TypeScript enforcing strict checks. Accidental removal of checks can lead to errors.

2: Custom Type Guards (Enhanced Reusability)

This method is a more elaborate, generalized version of the type assertion, but ultimately functions similarly.

const sheepNames = ['Capn Frisky', 'Mr. Snugs', 'Lambchop'] as const;
type SheepName = typeof sheepNames[number];

const unsafeJson = '"Capn Frisky"';

/**
 * Implement a custom type guard to confirm whether an unknown object is a SheepName.
 */
function isSheepName(maybeSheepName: unknown): maybeSheepName is SheepName {
    return typeof maybeSheepName === 'string' && sheepNames.includes(maybeSheepName);
}

/**
 * Retrieve a valid SheepName from a JSON-encoded string or raise an exception.
 */
function parseSheepName(jsonString: string): SheepName {
    const maybeSheepName: unknown = JSON.parse(jsonString);
    
    if (isSheepName(maybeSheepName)) {
        // The custom type guard confirms that this is a SheepName, satisfying TypeScript.
        return (maybeSheepName as SheepName);
    }
    
    throw new Error('The input data does not correspond to a sheep name.');
}

const definitelySheepName = parseSheepName(unsafeJson);

PRO: Enhanced reusability, slightly less fragile, potentially more readable.

CON: TypeScript still relies on manual verification. Seems like a cumbersome process for a simple task.

3: Utilize Array.find (Safer and Recommended Approach)

This method eliminates the need for type assertions, beneficial for those who prefer additional validation.

const sheepNames = ['Capn Frisky', 'Mr. Snugs', 'Lambchop'] as const;
type SheepName = typeof sheepNames[number];

const unsafeJson = '"Capn Frisky"';

/**
 * Retrieve a valid SheepName from a JSON-encoded string or raise an error.
 */
function parseSheepName(jsonString: string): SheepName {
    const maybeSheepName: unknown = JSON.parse(jsonString);
    const sheepName = sheepNames.find((validName) => validName === maybeSheepName);
    
    if (sheepName) {
        // `sheepName` originates from the list of `sheepNames`, providing assurance to the compiler.
        return sheepName;
    }
    
    throw new Error('The input does not represent a sheep name.');
}

const definitelySheepName = parseSheepName(unsafeJson);

PRO: Bypasses type assertions, leaving validation to the compiler. This aspect holds significance for me, making this solution my preference.

CON: Appears somewhat unusual and may pose challenges in terms of performance optimization.


In conclusion, you have the flexibility to choose from these strategies or explore recommendations from third-party libraries. It's worth noting that utilizing an array might not be the most efficient approach, especially for large datasets. Consider optimizing by converting the sheepNames array into a set for faster lookups (O(1)). Particularly useful when dealing with numerous potential sheep names or similar scenarios.

Answer №2

Ever since the release of Typescript 2.1, there has been a different approach available using the keyof operator.

The concept is simple. Due to the lack of string literal type information at runtime, you create a basic object with keys representing your string literals and then create a type based on those keys.

Here's an example:

// Values in this dictionary are not important
const myStrings = {
  A: "",
  B: ""
}

type MyStrings = keyof typeof myStrings;

isMyStrings(x: string): x is MyStrings {
  return myStrings.hasOwnProperty(x);
}

const a: string = "A";
if(isMyStrings(a)){
  // ... Treat a as if it was assigned with MyString type within this block: TypeScript compiler trusts our duck typing!
}

Answer №3

If your program contains multiple string union definitions that need runtime checking, you can use a generic function called StringUnion to create static types and type-checking methods for them.

Creating the Generic Function

// By using `extends string`, TypeScript infers a string union type from the literal values
// passed to this function. Without it, the values would be generalized to a common string type.
export const StringUnion = <UnionType extends string>(...values: UnionType[]) => {
  Object.freeze(values);
  const valueSet: Set<string> = new Set(values);

  const guard = (value: string): value is UnionType => {
    return valueSet.has(value);
  };

  const check = (value: string): UnionType => {
    if (!guard(value)) {
      const actual = JSON.stringify(value);
      const expected = values.map(s => JSON.stringify(s)).join(' | ');
      throw new TypeError(`Value '${actual}' is not assignable to type '${expected}'.`);
    }
    return value;
  };

  const unionNamespace = {guard, check, values};
  return Object.freeze(unionNamespace as typeof unionNamespace & {type: UnionType});
};

Usage Example

To merge the generated type definition with its namespace object and automatically have it available when imported into another module, add the following line of boilerplate code.

const Race = StringUnion(
  "orc",
  "human",
  "night elf",
  "undead",
);
type Race = typeof Race.type;

Example Scenario

At compile-time, the Race type functions just like a standard string union declaration with

"orc" | "human" | "night elf" | "undead"
. The .guard() function acts as a type guard, while .check() verifies validity and throws an error if necessary.

let r: Race;
const zerg = "zerg";

// Expected compiler error:
r = zerg;

// Expected run-time error:
r = Race.check(zerg);

// This block will not be executed:
if (Race.guard(zerg)) {
  r = zerg;
}

An Alternative Solution: runtypes

This method is inspired by the runtypes library, which offers similar functionality for defining various types in TypeScript and automatically generating run-time type checkers. It may require slightly more verbosity for this specific scenario but provides added flexibility.

Usage Example

import {Union, Literal, Static} from 'runtypes';

const Race = Union(
  Literal('orc'),
  Literal('human'),
  Literal('night elf'),
  Literal('undead'),
);
type Race = Static<typeof Race>;

The usage remains consistent with the previous example.

Answer №4

To handle this scenario, you can utilize the enum feature in TypeScript. This involves defining an enum with the possible decision options and then creating a type union to represent them.

export enum Decisions {
    approve = 'approve',
    reject = 'reject'
}

export type DecisionsTypeUnion =
    Decisions.approve |
    Decisions.reject;

if (decision in Decisions) {
  // This decision is valid
}

Answer №5

Implement a solution using the "array first" method, where you create string literals and simultaneously use Array.includes():

const StringsArray = ["X", "Y", "Z"] as const;
StringsArray.includes("X" as any); // true
StringsArray.includes("W" as any); // false

type MyStrings = typeof StringsArray[number];
let testing: MyStrings;

testing = "X"; // Acceptable
testing = "W"; // Compilation error

Answer №6

utilizing type is essentially creating Type Aliasing, however it does not appear in the compiled JavaScript code. Therefore, you cannot perform the following:

MyStrings.isAssignable("A");

Here's what you can achieve with it:

type MyStrings = "A" | "B" | "C";

let myString: MyStrings = getString();
switch (myString) {
    case "A":
        ...
        break;

    case "B":
        ...
        break;

    case "C":
        ...
        break;
        
    default:
        throw new Error("can only receive A, B or C")
}

In response to your query regarding isAssignable, you can do this:

function isAssignable(str: MyStrings): boolean {
    return str === "A" || str === "B" || str === "C";
}

Answer №7

My strategy involves creating a unique object type by utilizing the union type and initializing a placeholder instance of that object type. This allows for straightforward string type checking through type guards.

An advantage of this method is that whenever a new type is added or removed from the union, TypeScript will prompt us to update the object accordingly.

type MyLetters = "X" | "Y" | "Z";
type MyLettersObjectType = {
   [key in MyLetters ] : any
}
export const myLettersDummyObject : MyLettersObjectType = {
   X : "",
   Y : "",
   Z : "",
}
export const isValidLetter = (input: string): input is MyLetters => {
    return (input in myLettersDummyObject);
}

Usage :

if(isValidLetter("X")){  //true
   
}

if(isValidLetter("D")){  //false
   
}

Answer №8

Methods cannot be called on types during runtime, as types do not exist

MyStrings.isAssignable("A"); // This will not work — `MyStrings` is a string literal

Instead of this approach, it is recommended to create executable JavaScript code for input validation. It is the responsibility of the programmer to ensure that the function performs correctly.

function isMyString(candidate: string): candidate is MyStrings {
  return ["A", "B", "C"].includes(candidate);
}

Update

Following the suggestion from @jtschoonhoven, an exhaustive type guard can be created to verify if any string belongs to MyStrings.

Start by creating a function named enumerate which ensures all members of the MyStrings union are used. This will signal the need to update the type guard if the union is expanded in the future.

type ValueOf<T> = T[keyof T];

type IncludesEvery<T, U extends T[]> =
  T extends ValueOf<U>
    ? true
    : false;

type WhenIncludesEvery<T, U extends T[]> =
  IncludesEvery<T, U> extends true
    ? U
    : never;

export const enumerate = <T>() =>
  <U extends T[]>(...elements: WhenIncludesEvery<T, U>): U => elements;

The updated and enhanced type guard:

function isMyString(candidate: string): candidate is MyStrings {
  const valid = enumerate<MyStrings>()('A', 'B', 'C');

  return valid.some(value => candidate === value);
}

Answer №9

After exploring @jtschoonhoven's recommended approach, one can craft versatile factories to create parsing or validation functions:

const generateParserFactory = <DataType, T extends DataType>(data: readonly T[]): ((input: DataType) => T | null) => {
   return (input: DataType): T => {
       const match = data.find((item) => item === input)
       if (match) {
           return match
       }
       throw new InvalidDataError(data, input)
    }
}

Example of usage:

const catNames = ['Whiskers', 'Fluffy', 'Mittens'] as const
type CatName = typeof catNames[number]

const parseCatName = generateParserFactory(catNames)
let myCatName: CatName = parseCatName('Fluffy') // Valid
let willThrowError: CatName = parseCatName('Garfield') // Error thrown

Check out this working example on REPL

The challenge lies in ensuring the consistency of our type with how generateParserFactory constructs from our array of values.

Answer №10

Here's a different approach:

let myChoices = ['X', 'Y', 'Z'] as const;
type OptionsOne = typeof myChoices[number];

let moreChoices =  ['M', 'N', 'O'] as const;
type OptionsTwo = typeof moreChoices[number];

type AllOptions = OptionsOne | OptionsTwo;

let chosenOptions: Set<string> = new Set(myChoices);

function isChosen(option: AllOptions): option is OptionsOne {
  return chosenOptions.has(option);
}

This method offers better performance compared to using Array.find like some of the other suggestions.

Answer №11

There was a situation I encountered where I needed to validate all permitted query parameters and also implement type guards with the Union type for all allowed params.

My requirements were as follows:

  1. The method should be generic and adaptable for APIs with varying sets of permitted parameters. For example, one API may accept online|offline, while another could allow full|partial.
  2. An exception should be thrown if there's a parameter mismatch. Since this is used in an Express app, all API parameters need to be validated at runtime.
  3. I wanted to adhere to DRY principles and avoid situations where the allowed params differ across implementations.
  4. No reliance on external libraries 🙂

My solution meets all these criteria:

export function assertUnion<T extends string>(
    param: string,
    allowedValues: ReadonlyArray<T>
): param is T {
    if (!allowedValues.includes(param as T)) {
        throw new Error("Wrong value");
    }
    return true;
}

Here's how you can use it:

if (
    !assertUnion<"online" | "offline">(param, ["online", "offline"])
) {
    return;
}
console.log(param) // param will be of type "online" | "offline"

While it may seem like we're defining allowed types twice - once as a Union type and then in an array - because of the utility definition, you can add any additional parameter to the array. This ensures that your Union type remains the source of truth.

I would have liked to use

param is typeof allowedValues[number]
, but unfortunately, Typescript's const assertion only works with array literals and not with array parameters 😞

Similar questions

If you have not found the answer to your question or you are interested in this topic, then look at other similar questions below or use the search

The values obtained from an HTTP GET request can vary between using Curl and Ionic2/Angular2

When I make a GET request using curl in the following manner: curl https://api.backand.com:443/1/objects/todos?AnonymousToken=my-token I receive the correct data as shown below: {"totalRows":2,"data":[{"__metadata":{"id& ...

Can you explain the concept of cross referencing classes in Javascript/Typescript?

I am encountering difficulties with cross-referencing classes that are defined in the same file. // classes.ts export class A extends BaseModel implements IA { static readonly modelName = 'A'; b?: B; symbol?: string; constructor(object: ...

Every time I click on a single button, all the text inputs get updated simultaneously

One issue I encountered is with a component featuring increment and decrement buttons. These buttons are meant to interact with specific products, yet when clicked, all text inputs update simultaneously instead of just the intended one. COMPONENT HTML: &l ...

Cyrillic characters cannot be shown on vertices within Reagraph

I am currently developing a React application that involves displaying data on a graph. However, I have encountered an issue where Russian characters are not being displayed correctly on the nodes. I attempted to solve this by linking fonts using labelFont ...

The situation I find myself in frequently is that the Angular component Input

There seems to be an issue with a specific part of my application where the inputs are not binding correctly. The component in question is: @Component({ selector : 'default-actions', templateUrl : './default.actions.template.html&a ...

Is there a way to specify a type for a CSS color in TypeScript?

Consider this code snippet: type Color = string; interface Props { color: Color; text: string; } function Badge(props: Props) { return `<div style="color:${props.color}">${props.text}</div>`; } var badge = Badge({ color: &ap ...

The React type '{ hasInputs: boolean; bg: string; }' cannot be assigned to the type 'IntrinsicAttributes & boolean'

I recently started learning react and encountered an error while passing a boolean value as a prop to a component. The complete error message is: Type '{ hasInputs: boolean; bg: string; }' is not assignable to type 'IntrinsicAttributes & ...

Creating cohesive stories in Storybook with multiple components

I need assistance with my storybook setup. I have four different icon components and I want to create a single story for all of them instead of individual stories. In my AllIcons.stories.tsx file, I currently have the following: The issue I am facing is ...

What benefits could you derive from utilizing an interface to generate a fresh Array? (Pulumi)

Just delving into the world of TypeScript and Pulumi/IaC. I'm trying to wrap my head around a code snippet where an array of key values is being created using an interface: import * as cPulumi from "@company/pulumi"; interface TestInterface ...

Implementing CAPTCHA V2 on Angular encounters an Error: It requires the essential parameters, specifically the sitekey

Encountering an issue adding Recaptcha V2 to a simple Angular Page, my knowledge in Angular is limited. The HTML file and component.ts file are referenced below. Attempting to send this form along with the token to a Laravel API for validation, and return ...

Unable to position text in the upper left corner for input field with specified height

In a project I'm working on, I encountered an issue with the InputBase component of Material UI when used for textboxes on iPads. The keyboard opens with dictation enabled, which the client requested to be removed. In attempting to replace the textbox ...

What is the process for generating a new type that includes the optional keys of another type but makes them mandatory?

Imagine having a type like this: type Properties = { name: string age?: number city?: string } If you only want to create a type with age and city as required fields, you can do it like this: type RequiredFields = RequiredOptional<Propertie ...

The Ionic Android app seems to constantly encounter dark mode display issues

I'm currently working on a small app that includes a menu, some chips, and a search bar. The issue I'm facing is that I've applied the MD theme to the entire app like this: @NgModule({ declarations: [AppComponent], entryComponents: [], ...

Here's a method to extract dates from today to the next 15 days and exclude weekends -Saturday and Sunday

Is there a way to generate an array of dates starting from today and spanning the next 15 days, excluding Saturdays and Sundays? For example, if today is 4/5/22, the desired array would look like ['4/5/22', '5/5/22', '6/5/22' ...

The application was not functioning properly due to an issue with the getSelectors() function while utilizing @ngrx/entity to

Currently, I am facing an issue with implementing a NgRx store using @ngrx/entity library. Despite Redux Devtools showing my collection loaded by Effect() as entities properly, I am unable to retrieve any data using @ngrx/entity getSelectors. Thus, it seem ...

The shopping list feature is unable to save or list multiple recipes at once

My goal is to: Create a shopping list that retrieves recipes from an API. Transfer ingredients from one page to another. Automatically refresh/load data when more than 1 item is added. The challenge I am facing is: Only one set of ingredients loads. T ...

One inventive method for tagging various strings within Typescript Template Literals

As TypeScript 4.1 was released, many developers have been exploring ways to strictly type strings with predetermined patterns. I recently found a good solution for date strings, but now I'm tackling the challenge of Hex color codes. The simple approa ...

There was a problem with the module '@angular/material' as it was unable to export a certain member

In creating a custom Angular Material module, I have created a material.module.ts file and imported various Angular Material UI components as shown below: import { NgModule } from '@angular/core'; import { CommonModule } from '@angular/commo ...

What is the process of creating a new array by grouping data from an existing array based on their respective IDs?

Here is the initial array object that I have: const data = [ { "order_id":"ORDCUTHIUJ", "branch_code":"MVPA", "total_amt":199500, "product_details":[ { ...

Encountering an error while trying to update an Angular application to version 10

I am currently running a demo app and I am new to Angular. Below is my category list component. import { Component, OnInit } from '@angular/core'; import { Router } from '@angular/router'; import { Observable } from 'rxjs'; im ...