What is the best way to distinguish between tagged unions while narrowing down types using a type guard?

Exploring a Useful Helper Function

const isKeyOf = <K extends PropertyKey, O extends object>(key: K, object: O): key is K & keyof O => key in object

The isKeyOf function essentially narrows down the key type to determine if a key exists within an object

const obj = {
    a: 1,
    b: 2,
    c: 3
}

const key: string = "a" // the type of key is string

if (isKeyOf(key, obj)) {
    // the type of key would be narrowed to "a" | "b" | "c"
}

This approach works well unless the object is a union of types, like in this case:

const holder: Record<number, { a: number } | { b: number }> = {
    1: { a: 1 },
    2: { b: 2 }
}

// the type of obj becomes { a: number } | { b: number }
const obj = holder[1 as number]
const key: string = "a" // the type of key is string
 
if (isKeyOf(key, obj)) {
     // the type of key turns out to be never!
     // Interestingly, this branch does get executed and prints...
     console.log(typeof key) // prints string
     console.log(obj) // prints { a: 1 }
}

Further investigation revealed that the issue arises from the behavior of the keyof operator with union types

type objType = { a: number } | { b: number }
type key = keyof objType // = never

In such situations, how can we construct a type guard that accurately validates this and assigns the correct type to the key?

Answer â„–1

Like you pointed out, when using the keyof operator, it will only return the common keys. Therefore, with the following type, the result will be never:

// never
type Result = keyof ({ a: string } | { b: string });

To solve this issue, we need to individually check keyof for each member of the union. The corrected version should look like this:

// "a" | "b"
type Result = keyof { a: string } | keyof { b: string };

We can accomplish this by utilizing distributive conditional types. Union types are distributed whenever they are evaluated against conditions with extends. For example:

type Test<T> = T extends number ? T : never;

// 1
type Result = Test<'a' | false | [] | 1>

To ensure that we do not lose any members as seen in the previous example, we need to have a condition that is always true. Common conditions include checking against any or against T itself, since T extends T is always true:

type Test<T> = T extends T ?  keyof T : never;

// "a" | "b"
type Result = Test<{ a: string } | { b: string }>

Looks good! Let's adjust it for your type guard:

const isKeyOf = <K extends PropertyKey, O extends object>(
  key: K,
  object: O,
): key is K & (O extends O ? keyof O : never) => key in object;

Testing:

const obj = holder[1 as number];
const key: string = 'a'; // type of key is string

if (isKeyOf(key, obj)) {
  key; // "a" | "b"
}

playground

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

Modifying the Iterator Signature

My goal is to simplify handling two-dimensional arrays by creating a wrapper on the Array object. Although the code works, I encountered an issue with TypeScript complaining about the iterator signature not matching what Arrays should have. The desired fu ...

Utilizing Async and await for transferring data between components

I currently have 2 components and 1 service file. The **Component** is where I need the response to be displayed. My goal is to call a function from the Master component in Component 1 and receive the response back in the Master component. My concern lies ...

Is a package I overlooked? The 'findOne' property is not found within the 'Schema<Document<any, {}>, Model<any, any>, undefined>'

I have taken over the responsibility of maintaining the websites at my company, and I am encountering the error message (Property 'findOne' does not exist on type 'Schema<Document<any, {}>, Model<any, any>, undefined>' ...

Should Errors be Handled in the Service Layer or the Controller in the MVC Model?

Currently, I am in the process of developing a Backend using Express and following the MVC Model. However, I am uncertain about where to handle errors effectively. I have integrated express-async-errors and http-errors, allowing me to throw Errors anywher ...

What causes the string to be treated as an object in React Native?

I am fetching a string value through an API and I need to display it in the View. However, the string value is coming as a Promise. How can I handle this? "Invariant Violation: Objects are not valid as a React child (found: object with keys {_40, _65 ...

Is there a way to use TestCafé to simultaneously log into multiple services?

I've been experimenting with testcafé in an effort to simultaneously log into multiple services using the role mechanism. My goal is to have my tests logged into multiple services concurrently without switching between roles. While a guide on this t ...

The parameter type 'router' cannot be replaced with the type 'typeof ...'. The 'param' property is not included in the type 'typeof'

I'm currently working on a node application using TypeScript and have set up routing in a separate file named 'route.ts' import home = require('../controller/homeController'); import express = require('express'); let ro ...

The Next.js template generated using "npx create-react-app ..." is unable to start on Netlify

My project consists solely of the "npx create-react-app ..." output. To recreate it, simply run "npx create-react-app [project name]" in your terminal, replacing [project name] with your desired project name. Attempting to deploy it on Netlify Sites like ...

Set an enumerated data type as the key's value in an object structure

Here is an example of my custom Enum: export enum MyCustomEnum { Item1 = 'Item 1', Item2 = 'Item 2', Item3 = 'Item 3', Item4 = 'Item 4', Item5 = 'Item 5', } I am trying to define a type for the f ...

Retrieving a specific user attribute from the database using Angular

Currently, I am working on developing an application that utilizes ASP.NET for the Back End (API) and Angular for the Front End of the application. Within the API, I have set up controllers to retrieve either a list of users from the database or a single ...

What is the best way to handle missing values in a web application using React and TypeScript?

When setting a value in a login form on the web and no value is present yet, should I use null, undefined, or ""? What is the best approach? In Swift, it's much simpler as there is only the option of nil for a missing value. How do I handle ...

Creating a fresh JSON structure by utilizing an established one

I have a JSON data that contains sections and rubrics, but I only need the items for a new listing. The new object named 'items' should consist of an array of all the items. The final JSON output should be sorted by the attribute 'name&apos ...

Guide on how to create a promise with entity type in Nest Js

I am currently working on a function that is designed to return a promise with a specific data type. The entity I am dealing with is named Groups and my goal is to return an array of Groups Groups[]. Below is the function I have been working on: async filt ...

Which option is more beneficial for intercepting API data in Angular 6: interfaces or classes?

My API returns JSON data that is not structured the way I need it, so I have to make changes. { "@odata.context":"xxxxxx", "id":"xxxxxxxx", "businessPhones":[ ], "displayName":"name", "givenName":"pseudo", "jobTitle":null, "ma ...

A guide to iterating over an array and displaying individual elements in Vue

In my application, there is a small form where users can add a date with multiple start and end times which are then stored in an array. This process can be repeated as many times as needed. Here is how the array structure looks: datesFinal: {meetingName: ...

Angular - Set value on formArrayName

I'm currently working on a form that contains an array of strings. Every time I try to add a new element to the list, I encounter an issue when using setValue to set values in the array. The following error is displayed: <button (click)="addNewCom ...

The vertical scrolling functionality of the MUI DataGrid in Safari may occasionally fail to work

Utilizing the <Box> component from MUI core and the <DataGrid> component from MUIX, along with some other components, I have created a data grid that has the following appearance: https://i.sstatic.net/Gc8sP.png When the number of rows exceed ...

What function is missing from the equation?

I am encountering an issue with an object of type "user" that is supposed to have a function called "getPermission()". While running my Angular 7 application, I am getting the error message "TypeError: this.user.getPermission is not a function". Here is w ...

What causes the error message "Expected ':' when utilizing null conditional in TypeScript?"

UPDATE: After receiving CodeCaster's comment, I realized the issue was due to me using TypeScript version 3.5 instead of 3.7+. It was surprising because these checks seemed to be working fine with other Angular elements, such as <div *ngIf="pa ...

DiscordJS bot using Typescript experiences audio playback issues that halt after a short period of time

I am currently experiencing difficulties with playing audio through a discord bot that I created. The bot is designed to download a song from YouTube using ytdl-core and then play it, but for some reason, the song stops after a few seconds of playing. Bel ...