Type guard does not narrow down the union type

Explore the following code snippet:

type UnionType = 'foo' | 'bar' | 'baz'

const obj = {
    foo: 'huh',
    bar: 'hmm'
}

function func(input: UnionType) {
    if(input in obj) {
        input
    }
}

Interactive Demo

input is not being narrowed down to 'foo' | 'bar'. What could be causing this behavior?

Can you suggest a method to narrow the union without explicitly repeating the object keys?

Answer №1

In the realm of TypeScript, input in obj currently serves as a type guard for obj, but not for input. There is a pending feature request at microsoft/TypeScript#43284 requesting it to function as a type guard for input as well. However, implementation seems unlikely due to concerns mentioned in this comment, where it is deemed that such behavior could lead to issues with existing type guards.

It's important to note that this form of narrowing has always been viewed skeptically, as TypeScript types are not "sealed" (or "exact," as referred to in microsoft/TypeScript#12936). Just because input in obj returns true, it does not confirm that input is one of the known keys of typeof obj. To illustrate, consider the following alteration to your code:

const _obj = {
    foo: 'huh',
    bar: 'hmm',
    baz: 'oops'
}

const obj: { foo: string, bar: string } = _obj; // valid

Despite the change, the compilation proceeds without errors since the type {foo: string, bar: string} allows for additional properties. If TypeScript underwent the desired narrowing, input in obj would limit input to "foo" | "bar", yet, it might still be "baz". Thus, such a method of narrowing would pose risks.

This doesn't mean it should never be considered; after all, TypeScript is not entirely foolproof when it comes to type safety. Applying a similar argument, it wouldn't be entirely secure to narrow a value o of type {a: string} | {b: string} based on "a" in o, although it currently happens:

const a: { a: string } = { a: "" };
const _b = { b: "", a: 123 };
const b: { b: string } = _b;
const o = Math.random() < 0.5 ? a : b;
if ("a" in o) {
    o // narrowed to { a: string }, unsafely
    o.a.toUpperCase() 
}

While soundness is a crucial aspect, there are other factors to consider. Until any potential shift in perspective from the TS team occurs, the desired narrowing may not materialize. Alternately, finding workarounds becomes imperative.


One simple workaround involves crafting a user-defined type guard function that mimics the anticipated behavior of input in obj. In microsoft/TypeScript#43284, such a function is provided:

const in_ = <K extends string, O extends object>(key: K, object: O):
    key is K & keyof O => key in object;

By substituting in_(input, obj) for input in obj, you can observe the expected outcomes:

function func(input: UnionType) {
    if (in_(input, obj)) {
        input
        //^? (parameter) input: "foo" | "bar"
    } else {
        input
        //^? (parameter) input: "baz"
    }
}

The matter of soundness remains subject to your judgment based on the particular use case at hand.

Playground link to code

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

Using React to simulate API calls outside of testing environments

For instance, I encounter issues when certain endpoints are inaccessible or causing errors, but I still need to continue developing. An example scenario is with a function like UserService.getUsers where I want to use fake data that I can define myself. I ...

How is it possible for TypeScript to enable the importing of dependencies that it ultimately cannot utilize during runtime?

Take a look at my sample project by following this link: https://github.com/DanKaplanSES/typescript-stub-examples/tree/JavaScript-import-invalid I have developed a file named main.ts: import uuid from "uuid"; console.log(uuid.v4()); While type ...

Using NodeJS API gateway to transfer image files to S3 storage

I have been attempting to upload an image file to S3 through API Gateway. The process involves a POST method where the body accepts the image file using form-data. I crafted the lambda function in TypeScript utilizing the lambda-multipart-parser. While it ...

Waiting for asynchronous subscriptions with RxJS Subjects in TypeScript is essential for handling data streams efficiently

Imagine having two completely separate sections of code in two unrelated classes that are both listening to the same Observable from a service class. class MyService { private readonly subject = new Subject<any>(); public observe(): Observable&l ...

Can a TypeScript-typed wrapper for localStorage be created to handle mapped return values effectively?

Is it feasible to create a TypeScript wrapper for localStorage with a schema that outlines all the possible values stored in localStorage? Specifically, I am struggling to define the return type so that it corresponds to the appropriate type specified in t ...

Understanding the specific types of subclasses derived from an abstract generic class in Typescript

There is a Base generic class: abstract class BaseClass<T> { abstract itemArray: Array<T>; static getName(): string { throw new Error(`BaseClass - 'getName' was not overridden!`); } internalLogic() {} } and its inherito ...

How to effectively send an HTTP GET request to a REST API in Angular 2 and save the response in a JSON object

Currently, I am attempting to execute a GET request to the GitHub API using Angular2. The returned data is in JSON format, and my goal is to store it in a JSON object for further processing. While referring to the Angular2 documentation for guidance, I en ...

What is the best way to customize fonts for PDFMake in Angular projects?

Recently, I delved into the PDFMake documentation in hopes of creating a document for my Angular application. Along the way, I stumbled upon queries like this one, but unfortunately, found no answers. I am curious if anyone can offer insight or provide a ...

Having trouble getting web components registered when testing Lit Element (lit-element) with @web/test-runner and @open-wc/testing-helpers?

Currently, I am working with Lit Element and Typescript for my project. Here are the dependencies for my tests: "@esm-bundle/chai": "^4.3.4-fix.0", "@open-wc/chai-dom-equals": "^0.12.36", "@open-wc/testing-help ...

What is the correct data type for the vuex store that is passed to the vuex plugin when it is being initialized?

Here is how the store initialization process is currently being implemented: /* Logic */ import Vue from 'vue' import Vuex, { StoreOptions } from 'vuex' Vue.use(Vuex) const DataManager = new UserDataManager() type RootStateType = { ...

Updating Angular 5 Views Dynamically Using a While Loop

I am facing an issue in my app where I have a progress bar that updates using a while loop. The problem is that the view only updates after the entire while loop has finished running, even though I am successfully changing the update progress value with ea ...

The specified type '{ state: any; dispatch: React.Dispatch<{ type: string; value: any; }>; }' is not compatible with the expected type

I've been working on a UI layout that includes checkboxes on the left, a data table on the right, and a drop zone box. The aim is to keep the table data updated whenever a new file is dropped, and also filter the data based on checkbox selection. I ma ...

Tips for fixing TypeScript compiler error TS2339: Issue with accessing 'errorValue' property in Angular 5 project

Within a component, I have developed a function to manage errors returned from a Rest Service and determine the corresponding error message to display to the user. This method accepts an error object (custom data structure from the service), navigates to e ...

Tips for maintaining an open ng-multiselect-dropdown at all times

https://www.npmjs.com/package/ng-multiselect-dropdown I have integrated the ng multiselect dropdown in my Angular project and I am facing an issue where I want to keep it always open. I attempted using the defaultOpen option but it closes automatically. ...

OCI: Predict expenses based on a selection of virtual machines

Seeking to determine the anticipated cost of a selection of instances within OCI utilizing the TypeScript SDK. Oracle offers a tool called Cloud Cost Estimator for configuring and dynamically displaying cost estimates. Is it possible to achieve this throug ...

Playing around with Segment Analytics testing using Jest in TypeScript

I've been struggling to write a unit test that verifies if the .track method of Analytics is being called. Despite my efforts, the test keeps failing, even though invoking the function through http does trigger the call. I'm unsure if I've i ...

Different ways to invoke a general method with a deconstructed array as its argument

Calling a Generic Method with Deconstructed Array Parameters As of today, the only method to ensure typed safe inherited parameters is by using a deconstructed array and explicitly defining its type. This allows calling the parent method by utilizing the ...

Using 3 dots argument in Angular 5 to assign values to an array

I stumbled upon this line of code in Angular. Can someone explain its meaning? this.columns = [...this.columns, col]; My guess is that this relates to the immutable concept of arrays. ...

syncfusion export pdf demonstrating the toggle button's current state

Currently, I am using syncfusion for converting my page to PDF format. I have a toggle button that is default set to true. However, regardless of the actual state of the toggle button, it always appears as on (true) when exported to PDF. I attempted to s ...

Encountered a Solana Error while handling Instruction 2: custom program error with code 0x1000078

While attempting to create an AMM Pool using the 'ammCreatePool.ts' script from the raydium-sdk repository, I encountered the following error message. I executed the script directly with ts-node src/ammCreatePool.ts and made modifications so that ...