Using TypeScript to narrow down a type union based on the return types of functions

Why does Typescript struggle to differentiate a type union composed of return types from functions without explicitly declaring the return type on the function?

In this scenario, when I don't specify the return values of the event creator functions, the union type cannot be narrowed down.

enum EventType {
  FOO = "foo",
  GOO = "goo",
}

function createFooEvent(args: {
  documentId: number | null
}) {
  return {
    type: EventType.FOO,
    data: args
  }
}
function createGooEvent(args: {
  id: number
  isSelected: boolean
}) {
  return {
    type: EventType.GOO,
    data: args
  }
}

type EventArgType =
  | ReturnType<typeof createFooEvent>
  | ReturnType<typeof createGooEvent>

function eventHandler(event: EventArgType) {
  switch(event.type) {
    case EventType.FOO: {
      // The issue arises as `event` contains `data`, but `data`'s type is a union and remains undiscriminated
      event.data;
        break
    }
  }
}

However, specifying the return types as shown below allows for successful discrimination of the union:

function createFooEvent(args: {
  documentId: number | null
}): {
  type: EventType.FOO,
  data: {
    documentId: number | null
}} {
  return {
    type: EventType.FOO,
    data: args
  }
}
function createGooEvent(args: {
  id: number
  isSelected: boolean
}): {
  type: EventType.GOO,
  data: {
    id: number
    isSelected: boolean
}} {
  return {
    type: EventType.GOO,
    data: args
  }
}

Click here to view an example in TS playground.

Answer №1

In TypeScript, the constant is not inferred as the type by default: https://www.typescriptlang.org/docs/handbook/release-notes/typescript-3-4.html#const-assertions

For instance:

var a = 'test'

TypeScript will infer the type of a as string, not as 'test'.

To correct this, use as const:

var a = 'test' as const;

With this change, a will be of type 'test'.

The same principle applies to your code:

function createFooEvent(args: {
  documentId: number | null
}) {
  return {
    type: EventType.FOO,
    data: args
  };
}

The function's return type is {type: EventType} instead of {type:'foo'}.

By adding as const to the return type, it will behave as desired. You can see it in action on TS Playground

function exampleOne(){
  enum EventType {
    FOO = "foo",
    GOO = "goo",
  }

  function createFooEvent(args: {
    documentId: number | null
  }) {
    return {
      type: EventType.FOO,
      data: args
    } as const;
  }
  function createGooEvent(args: {
    id: number
    isSelected: boolean
  }) {
    return {
      type: EventType.GOO,
      data: args
    } as const;
  }

  type EventArgType =
    | ReturnType<typeof createFooEvent>
    | ReturnType<typeof createGooEvent>

  function eventHandler(event: EventArgType) {
    switch(event.type) {
      case EventType.FOO: {
        // event.data in this case will be {documentId: number|null}
        event.data;
        break
      }
    }
  }
}

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

How to prevent duplicate database entries in Angular forms?

Currently, I am working on a project using Angular and TypeScript. The goal is to retrieve a list of users from an API and allow for the addition of new users. However, I am struggling with determining how to verify if a user with a specific name already e ...

What is the specific event type triggered by the onError event when utilizing an img tag?

I'm attempting to display an image. If the URL fails to load, I want to show a different image instead. Currently, my code is functioning properly, but I am utilizing type "any" for the event. What should be the appropriate type for the event? functi ...

What is the best way to strip out a changing segment of text from a string?

let: string str = "a=<random text> a=pattern:<random text (may be fixed length)> a=<random text>"; In the given string above, let's assume that a= and pattern are constants. It is possible that there may or may not be a ...

Navigate through the router using query parameters in Angular 5

Encountering an issue with routing to a route containing query params. Here is the function in question: goToLink(link) { this.router.navigate([`${link.split('?')[0]}`, { queryParams: this.sortParams(link)}]); } Additionally, there is anoth ...

Steps for confirming whether each element in the array includes the specified search string using Typescript and protractor

How can I verify if each element in an array contains a specific search string in Typescript/Protractor? The issue I faced was that the console statements were returning false because they searched for exact matches instead of the search string. Any sugg ...

In my attempt to assess the correlation between value 1 and a value in the preceding object, I am utilizing the *ngFor directive

Attempting to compare 2 entries in an *ngFor loop. The code should compare the value at the current object to a value at the previous object. <ng-container *ngFor="let item of s_1.comments[0]; index as b"> <article class="message i ...

What is the best way for me to determine the average number of likes on a post?

I have a Post model with various fields such as author, content, views, likedBy, tags, and comments. model Post { createdAt DateTime @default(now()) updatedAt DateTime @updatedAt id String @id @default(cuid()) author U ...

What is the mechanism behind the widening of object literal types in Typescript inference?

I've been reading up on how typescript broadens inferred types but I'm still not entirely clear about what's happening here: type Def = { 'T': { status: 5, data: {r: 'm'}}, } function route<S extends keyof Def> ...

Converting a JSON array into a TypeScript array

Looking to convert a JSON array into a TypeScript variable array. The JSON data retrieved from http://www.example.com/select.php: { "User":[ {"Name":"Luca M","ID":"1"}, {"Name":"Tim S","ID":"2"}, {"Name":"Lucas W","ID":"3"} ...

Converting Typescript fat arrow syntax to regular Javascript syntax

I recently started learning typescript and I'm having trouble understanding the => arrow function. Could someone clarify the meaning of this JavaScript code snippet for me: this.dropDownFilter = values => values.filter(option => option.value ...

Exploring the MVVM architecture in React and the common warning about a missing dependency in the useEffect hook

I'm currently in the process of developing a React application using a View/ViewModel architecture. In this setup, the viewModel takes on the responsibility of fetching data and providing data along with getter functions to the View. export default f ...

Expo + tRPC: Oops! Looks like the application context couldn't be retrieved. Don't forget to wrap your App inside the `withTRPC` HoC for

I'm currently working on a straightforward tRPC server setup: // server.ts import { initTRPC } from "@trpc/server"; import { z } from "zod"; const t = initTRPC.create(); export const appRouter = t.router({ greeting: t.procedu ...

Who is the intended audience for the "engines" field in an npm package - consumers or developers?

As the creator of an npm library, I have included the current LTS versions of Node.js and npm in the package manifest under the engines field. This ensures that all contributors use the same versions I utilized for development: Node.js <a href="/cdn-cgi ...

Ways to stop users from submitting a form repeatedly in Angular

In my current feature, users have the ability to create new data and save it. If the data already exists, a modal will pop up as shown in the image below: However, there is an issue when the user clicks the save button multiple times while the request is ...

Create a TypeScript module that exports classes from multiple files

I've been facing a challenge with splitting TypeScript modules containing classes into separate files. Despite searching for solutions, none have resolved my specific issue. Currently, I have a module called store that contains two classes: Person an ...

What is the significance of the double exclamation mark operator in determining the truthiness or falsiness of an object's values?

Trying to determine the truthiness or falsiness of an object is proving to be a challenge for me. If an object contains all truthy values, I want one outcome; if it contains falsy values such as 0, an empty string, or undefined, I want a different outcom ...

Showing the outcome of the request from the backend on an HTML page using the MEAN stack

I am currently in the process of developing an angular application with a node.js + express backend. After successfully retrieving the necessary data from MongoDB and being able to view it through terminal, I encountered a challenge when trying to display ...

guide on transferring csv information to mongoDB using Angular and Node.js

I have a CSV file that contains data which I need to transfer into MongoDB using Angular and Node.js. Seeking assistance with reading the data from the CSV file using Angular, parsing it, and storing it in MongoDB. import { Injectable } from '@ang ...

NextJS: Route Handler encountering Method Not Allowed (405) error when trying to redirect

Current NextJs version is 13.4.3 I have set up a route handler to capture POST requests. For more information on route handlers, please refer to the documentation at [https://nextjs.org/docs/app/building-your-application/routing/router-handlers] In this ...

Displaying Information in Angular Modal Windows

I'm facing an issue while trying to create an edit button for a formGroup that is initially saved. When the user clicks on the adjacent button, a modal should open with editable data. However, I encountered this error and haven't been able to res ...