Eliminate the use of type assertion when verifying if a value is included in a union

I have a unique scenario where I am using a union type that involves an array. I need to check the values at run-time, but TypeScript is requiring me to use a type-assertion in this case. Take a look at the following code:

const Pets = ["dog", "cat"] as const
type Pet = typeof Pets[number]

type Animal = Pet | "tiger"

function checkDanger(animal: Animal) {
  if (Pets.includes(animal as Pet)) {
        return "not dangerous"
    }
    return "very dangerous"
}

The issue lies with the as Pet part.

If I remove that, I will receive:

Argument of type 'Animal' is not assignable to parameter of type '"dog" | "cat"'. Type '"tiger"' is not assignable to type '"dog" | "cat"'.

In a more intricate real-world scenario, such an assertion could have unwanted consequences. Is there a way to achieve this without needing an assertion?

Answer №1

This particular issue is a well-documented one that involves the use of includes. For more details, you can refer to the discussion on issues/26255.

Fortunately, there exists a workaround for this problem. You have the option to create a custom curried typeguard:

const inTuple = <Tuple extends string[]>(
    tuple: readonly [...Tuple]) => (elem: string
    ): elem is Tuple[number] =>
        tuple.includes(elem)

// (elem: string) => elem is "dog" | "cat"
const inPets = inTuple(Pets)

Let's put it into practice:

const Pets = ["dog", "cat"] as const
type Pet = typeof Pets[number]

type Animal = Pet | "tiger"

const inTuple = <Tuple extends string[]>(
    tuple: readonly [...Tuple]) => (elem: string
    ): elem is Tuple[number] =>
        tuple.includes(elem)

// (elem: string) => elem is "dog" | "cat"
const inPets = inTuple(Pets)

function checkDanger(animal: Animal) {
    if (inPets(animal)) {
        animal // "dog" | "cat"
        return "not dangerous"
    }
    return "very dangerous"
}

Given the presence of a conditional statement, there may be an opportunity to enhance the function through overloading and narrowing the return type:

const Pets = ["dog", "cat"] as const
type Pet = typeof Pets[number]

type Animal = Pet | "tiger"

const inTuple = <Tuple extends string[]>(
    tuple: readonly [...Tuple]) => (elem: string
    ): elem is Tuple[number] =>
        tuple.includes(elem)

// (elem: string) => elem is "dog" | "cat"
const inPets = inTuple(Pets)


function checkDanger(animal: Pet): "not dangerous"
function checkDanger(animal: Animal): "very dangerous"
function checkDanger(animal: string) {
    if (inPets(animal)) {
        animal // "dog" | "cat"
        return "not dangerous"
    }
    return "very dangerous"
}

const result = checkDanger('tiger') // very dangerous
const result2 = checkDanger('cat') // not dangerous

Interactive Playground The order of overload signatures plays a crucial role.

You may have observed that there are no explicit type assertions within my code.

The functionality of the inTuple typeguard stems from the fact that inside the function body, tuple is treated as an array of strings. This allows the operation to be valid, since tuple[number] and elem are mutually assignable.

const inTuple = <Tuple extends string[]>(
    tuple: readonly [...Tuple]) =>
    (elem: string): elem is Tuple[number] => {
        tuple[2] = elem // ok
        elem = tuple[3] // ok
        return tuple.includes(elem)
    }

Answer №2

Regrettably, the limitation lies within the typescript compiler. However, there is a more efficient way to handle this issue instead of repetitively writing animal as Pet in numerous functions. You can employ Type Guards like so:

function isPet(something: Pet | Animal) : something is Pet {
    return Pets.includes(something as Pet)
} 

function checkDanger(animal: Animal) {
    if (isPet(animal)) {
        return "not dangerous"
    }
    return "very dangerous"
}

By adopting this method, it becomes easier to refactor your code when renaming type definitions. Additionally, using a read-only array may limit flexibility and necessitate type assertion.

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

What is the best way to enable code sharing between two TypeScript projects housed within the same repository?

Our project has the following structure: api (dir) -- package.json -- tsconfig.json -- src (dir) -- client (dir) ---- package.json ---- tsconfig.json ---- src (dir) The "client" directory houses a create-react-app project that proxies to the API d ...

The parameter 'X' cannot be assigned to the argument of type 'X'

Hello there! I'm diving into Type Script for the first time with VSCode. Encountering these errors: error TS2322: Type '() => string' is not assignable to type 'string'. error TS2322: Type '() => number' is ...

What is the proper syntax for invoking a function using an event in Angular?

I have a pair of elements: pictures and camera, with camera as the child element. Within the camera element, I have the following TypeScript snippet (abbreviated): @Component({ selector: 'app-camera', templateUrl: './camera.component.ht ...

Localization of labels and buttons in Angular Owl Date Time Picker is not supported

When using the Owl Date Time Picker, I noticed that the From and To labels, as well as the Set and Cancel buttons are not being localized. Here is the code snippet I am using to specify the locale: constructor( private dateTimeAdapter: DateTimeAdapter&l ...

Using an object hierarchy in Typescript to create an if statement

Currently, I am attempting to create a logical statement using a hierarchy structure as shown below: if ((config.elementConfig.curve[0].dataset[0].splitBy = 'my discrete var')) {..... However, when implementing this statement, I encounter the er ...

What is the best way to refresh a Component upon sending data to the server?

I'm in the process of developing a cross-platform application. I have a TabView Component that needs to update a tab after sending data to the server. During the initialization (ngOnInit) phase, I dynamically set the content of my tab. However, when I ...

What is the best way to remove specific records from a FirebaseTS Database using Angular 13?

Hi there! I'm still getting the hang of the Angular framework, so please bear with me. Currently, I have a feed or timeline consisting of simple posts that contain text. This text is stored in Firebase using the following method: firestore = new Fireb ...

Converting a JSON array stored in a local file to a TypeScript array within an Angular 5 project

I'm currently working on developing a web app using Angular 5. My JSON file has the following structure: [ { "id": 0, "title": "Some title" }, { "id": 1, "title": "Some title" }, ... ] The JSON file is store ...

Error Message: ElectronJS - Attempted to access the 'join' property of an undefined variable

I am currently working on developing a tray-based application. However, I am encountering an error that reads: Uncaught TypeError: Cannot read property 'join' of undefined Can anyone guide me on how to resolve this issue? This is the content ...

Could you please explain the specific distinctions between pipe and map within Angular 7?

After extensive research, I'm still struggling to understand the distinction between pipe and map in Angular 7. Should we always include a pipe in Service.ts file in Angular 7? Appreciate any clarification on this matter. ...

Limit the outcomes of the Ionic timepicker

Currently, I am utilizing the ionic datetime feature. However, instead of receiving just the hours, minutes, and seconds, the result I am getting looks like this 2020-10-05T00:00:27.634+07:00. What I actually require from this output is only 00:00:27. Is ...

Tips for positioning a div alongside its parent div

I have a unique setup with two nested divs that are both draggable. When I move the parent div (moveablecontainer), the child div (box.opened) follows smoothly as programmed. Everything works well in this scenario. However, when I resize the browser windo ...

Using React and TypeScript to pass member variables

My component Child has a member variable that can change dynamically. While I am aware of passing props and states, is there a more elegant solution than passing member variables through props or other parameters? class Child extends React.Component< ...

Utilizing process.env in TypeScript can be a bit tricky as dot notation is not effective for accessing its properties

When I set my scripts to: "start": "NODE_ENV=development nodemon dist/Server.js", I am encountering an issue when trying to access NODE_ENV in my code. Both dot and bracket notation return undefined: The dependencies in my project are: "@types/node": "^8. ...

Acquire data on the type from the returned outcome of a function

Below is a function that I am working with in the TypeScript playground: function myf(): Record<string, string> { return { prop1: "a", prop2: "b" } } This function is pure and simply returns a dictionary value. My goal is to ext ...

Entering key-value pairs into a dictionary to show correlation

I've been struggling to find a solution for what seems like a simple issue. The problem lies in typing a dictionary with values of different types so that TypeScript can infer the type based on the key. Here is the scenario: type Id = string; inter ...

Error: An uncaught TypeError has occurred in a TypeScript script using three.js, stating that it is unable to read the 'position' property of

I'm encountering a small issue with initializing the camera position in my three.js project written in TypeScript. The browser console is showing me an error message: Uncaught TypeError: Cannot read property 'position' of undefined. Below is ...

How to use attributes in Angular 2 when initializing a class constructor

Is there a way to transfer attributes from a parent component to the constructor of their child components in Angular 2? The process is halfway solved, with attributes being successfully passed to the view. /client/app.ts import {Component, View, bootst ...

Experiencing code coverage challenges in Angular 8 relating to function properties

https://i.sstatic.net/WQpVB.png Looking for suggestions on how to create a unit test case in Jasmine to address the code coverage problem. Any ideas? ...

What is the best way to filter specific data types when using ngFor in Angular?

As I loop through the array named "fruits," which contains objects of type "FruitService" that I created, I want to display each element. However, when I delete them (I know it's strange, no database involved), they turn into type "undefined" and star ...