Restrictive discriminated union via function argument

I am in possession of a shop that organizes a variety of types based on their IDs

interface Dog { type: "dog"; woofs: string; }
interface Cat { type: "cat"; meows: string; }
type Pet = Dog | Cat;
type AnimalState = Record<string, Pet>

My aim is to create a function that can output a specific pet type based on its key/ID.

function getSpecificAnimal(state: AnimalState, key: string, type: Pet["type"]) {
  const pet = state[key];

  if (pet.type === type) {
    return pet;
  }

  throw new Error("pet was wrong type");
}

const aDog = getSpecificAnimal(state, "a", "dog"); // should return type Dog, or throw an exception

Unfortunately, I am struggling to instruct Typescript to narrow down the return value of the getSpecificAnimal function.

I have experimented with generics and a type map to correlate all the arguments, but Typescript remains unsatisfied.

type PetTypeMap = {
  dog: Dog,
  cat: Cat,
}
function getAnimalOfType<T extends Pet, K extends T["type"]>(animals: AnimalState, id: string, type: K): PetTypeMap[K] {
  const pet = animals[id];
  //    ^?

  if (pet.type === type) {
    return pet;
  }

  throw new Error("wrong pet type");
}

Despite my attempts, but to no avail. Am I unable to achieve this type of narrowing with Typescript?

Answer №1

Implementing getAnimalOfType() with compiler-verified type safety in TypeScript 4.7 is not feasible. To have the return type depend on its inputs, you would need to either create an overloaded function or a generic one. While callers will be satisfied with either approach, the compiler struggles to provide assistance within the implementation.


Overloads are simple to write and use:

function getSpecificAnimal(state: AnimalState, key: string, type: "dog"): Dog;
function getSpecificAnimal(state: AnimalState, key: string, type: "cat"): Cat;
function getSpecificAnimal(state: AnimalState, key: string, type: Pet["type"]) {
    const pet = state[key];

    if (pet.type === type) {
        return pet; // no error
    }

    throw new Error("pet was wrong type");
}

const state = {
    a: { type: "dog", woofs: "a lot" },
    b: { type: "cat", meows: "incessantly" }
} as const;
const aDog = getSpecificAnimal(state, "a", "dog");
// const aDog: Dog
console.log(aDog.woofs.toUpperCase()) // A LOT

However, their implementations are loosely checked (refer to: microsoft/TypeScript#10765). Therefore, you must ensure correct implementation despite the lack of compiler warnings.

...

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

Creating a Loading Sign with a Button Component in React

Request Description: In my form, I have a button that triggers a submission to the backend. While the request is processing, I want the button to display a loading indicator instead of the usual text. Once the request is complete, I need the form to disap ...

Enhancing JSON Formatting with Angular 4 and Typescript

In the process of developing my Angular 4 application, I am interfacing with a REST API through JSON requests. As I work on creating JSON objects to send via POST requests, I find myself putting in quite a bit of manual effort to construct them... I KNOW ...

Refine a union type by considering the properties already defined in an object

interface CustomHTMLElement { htmlPropA: string, htmlPropB: string, } interface CustomHTMLInput { inputPropA: string, inputPropB: string, } type CustomElement = | CustomHTMLElement | CustomHTMLInput const element: CustomElement = { inputPr ...

Stop committing changes in Git when there are any TypeScript errors found

While working on my project in TypeScript using Visual Code, I encountered a situation where I was able to commit and push my changes to the server through Git (Azure) even though there was an error in my code causing a build failure. It made me wonder i ...

Encountering a runtime issue with socket.io when using typescript that has been bundled by

Recently, I attempted to implement web sockets using socket.io in a Node server written in TypeScript with ExpressJS and bundled with Webpack. The server code is structured as follows: import * as Express from "express"; import * as SocketIO from "socket ...

Understanding the significance of an exclamation point preceding a period

Recently, I came across this code snippet: fixture.componentInstance.dataSource!.data = []; I am intrigued by the syntax dataSource!.data and would like to understand its significance. While familiar with using a question mark (?) before a dot (.) as in ...

How come I'm able to access the form's control within setTimeout but not outside of it?

Having just started working with Angular, I came across a strange issue involving forms and setTimeout. When trying to access the form control of an input element inside setTimeout within the OnInit lifecycle hook, it works fine. However, when attempting t ...

Initializing variables in Angular2 templates

I am facing an issue where, upon running the application, the console displays all the logs from the ngOnInit function, but the actual view only shows a skeleton without the component variables and text from l18n. It seems like the ngOnInit is not working ...

Launching the Skeleton feature in NextJS with React integration

I have been working on fetching a set of video links from an Amazon S3 bucket and displaying them in a video player component called HoverVideoPlayer. However, during the loading process, multiple images/videos scale up inside a Tailwind grid component, ca ...

What is the quickest method for setting up types for node packages?

Whenever I need to use typed packages in my Node.js projects, there are two steps I have to take: Firstly, install the original package. For example: npm install express -S Secondly, install its type definition package. npm install @types/express -D I f ...

Using Checkboxes in React with Material-UI

I am currently facing an issue with my 2 checkboxes. Whenever I select one, both of them get selected automatically. I want the user to be able to choose one, both, or none at all. It's puzzling why they are both getting selected simultaneously. Here ...

How to Halt or Keep Running a For Loop in Angular 2/4?

for (let index = 0; index < this.selectedFileType[i].count; index++) { this.modal.show(); } My goal is to display the modal each time it enters the loop, and then proceed with the loop after closing the modal. ...

Seeking a quick conversion method for transforming x or x[] into x[] in a single line of code

Is there a concise TypeScript one-liner that can replace the arrayOrMemberToArray function below? function arrayOrMemberToArray<T>(input: T | T[]): T[] { if(Arrary.isArray(input)) return input return [input] } Trying to cram this logic into a te ...

Swap out the selector of an Ionic2 component with its contents

I am utilizing Ionic2 along with TypeScript. Let's assume I desire a custom component to include the content of an ion-menu. <sidemenu></sidemenu> //This sidemenu will contain the ion.menu. <ion-nav id="nav" [root]="rootPage" ...

Retrieving a value in the app component from a map using Angular

I have been struggling to display the values of ValueM, ValueR, and product in my app.component.html file. Can anyone offer a solution or tip to help me move forward? Thank you very much. app.component.ts forkJoin( this.service1.method1(filter1), ...

Add an asterisk before each line of comment when working in a TypeScript file using the VS Code IDE

Within my VS Code workspace, I am using the Typescript language and would like to format my comments across multiple lines with a specific style (look out for the star character) /** *@desc any text * any text */ However, when I attempt to write a comm ...

What is the best way to search for an Enum based on its value?

One of my challenges involves an enum containing various API messages that I have already translated for display in the front-end: export enum API_MESSAGES { FAILED_TO_LOAD = 'Failed to load data', TOKEN_INVALID = 'Token seems to be inva ...

The Crimson Thread when incorporating tsx into Next.js

https://i.sstatic.net/zXvPT.png While working with TSX in React and TypeScript, I encountered an issue. A red line appeared on the screen even though the project runs successfully. Can anyone explain why this red line is appearing and why the classes in T ...

How to access type properties in typescript without using the "this" keyword

Below is a snippet of code that I am working with: class Player implements OthelloPlayer { depth; constructor(depth: number) { this.depth = depth; } getMove(state: OthelloState) { return this.MinimaxDecision(stat ...

Running an HTTP request conditionally within combineLatest

I'm working on a combineLatest function that makes 2 http requests, but I only want the second request to be made if a specific condition is satisfied. combineLatest([ this.leadsService.fetchALLLeadsActivityChart(this.clientId, this.getParams(option ...