Tips for obtaining type narrowing for a function within a mixed array

In my coding adventure, I have crafted a brilliant match function. This function is designed to take a value along with an array of [case, func] pairs. The value is then compared to each case, and if a match is found, the associated func is executed with the value as a parameter:

type Case<T> = [T | Array<T>, (value: T) => void];

const match = <V>(value: V, cases: Array<Case<V>>) => {
  for (const [key, func] of cases) {
    if (Array.isArray(key) ? key.includes(value) : key === value) {
      return func(value);
    }
  }
};

My next goal is to ensure that the value passed to each func is narrowed down to its corresponding case. Here's how it should work:

// const value: string = "whatever";
match(value, [
  [
    "something",
    (v) => {
      // here, `v` should narrow down to `"something"`
    }
  ],
  [
    ["other", "thing"],
    (v) => {
      // here, `v` should be limited to `"other" | "thing"`
    }
  ]
] as const);

Answer №1

If you try to specify the correct typings for match(), unfortunately, TypeScript will not infer the callback parameter types as desired. This feature is currently missing in TypeScript, and there are a few options available to deal with it - waiting for implementation (which may never happen), refactoring your code in some way, or simply giving up on that particular functionality.


The accurate typing for match() function can be seen below:

const match = <V, C extends V[]>(
  value: V,
  cases: [...{ [I in keyof C]: Case<C[I]> }]
) => {
  for (const [key, func] of cases) {
    if (Array.isArray(key) ? key.includes(value) : key === value) {
      return func(value as never);
    }
  }
};

In this code snippet, the type of cases is not just merely Array<Case<V>>, as that would not accurately track the subtypes of V at each element of the array. Instead, a mapped array type over the new generic type parameter C is used, which is constrained to be a subtype of V[]. Each element of C is wrapped with Case<⋯> to get the corresponding element of cases.


By calling the function like so:

const value: string = "whatever";
match(value, [
  [ "something", (v) => {} ],
  [ ["other", "thing"], (v) => { }]
]);

the compiler correctly infers V to be string, but it fails to infer C properly. It defaults to just string[], causing both instances of v within the callbacks to be of type

string</code, which isn't ideal.</p>
<hr />
<p>To work around this limitation, one possible solution is to use a helper function inside the call to <code>match()
to infer the cases incrementally instead of all at once. For example:

match(value, [
  c("something", (v) => { }),
  c(["other", "thing"], (v) => { })
]);

where c is defined as:

const c = <const T,>(
  value: T | Array<T>,
  handler: (value: T) => void
): Case<T> => [value, handler];

This workaround helps the compiler to infer the specific types individually, making it more manageable overall, even though it requires an extra step when calling the function.

While this workaround may seem cumbersome, it provides a lightweight solution to the problem at hand until a more direct approach becomes available.

It's important to note that these workarounds depend on the specific use case and may or may not be suitable for every scenario.

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

Issues arise when using Android BluetoothLeAdvertiser in Nativescript applications

I've been working on creating a Nativescript application that can send Bluetooth low energy advertisements. Since there are no existing Nativescript plugins for this functionality, I decided to develop a Java library (with plans to add a Swift library ...

Is it feasible to obtain the userId or userInfo from the Firebase authentication API without requiring a login?

Is it feasible to retrieve the user id from Firebase authentication API "email/password method" without logging in? Imagine a function that takes an email as a parameter and returns the firebase userId. getId(email){ //this is just an example return t ...

The method of having two consecutive subscribe calls in Angular2 Http

Can the Subscribe method be called twice? I am attempting to create an API factory that stores data in the factory and allows different components to use that data for each AJAX call. The factory: export class api { result = []; constructor (p ...

typescript - transforming text into numerical values

newbalance = (Number(this.balance)) + (Number(this.pastAmount)); The result for my newbalance calculation is coming back as undefined, even though this.balance is 34 and this.pastAmount is 23. I've set this up in the controller and I'm trying t ...

Is there a way to convert a typescript alias path to the Jest 'moduleNameMapper' when the alias is for a specific file?

I am currently working on setting up Jest in a TypeScript project. In our tsconfig.json file, we are using path aliases like so: "baseUrl": ".", "paths": { "@eddystone-shared/*": [ "../shared/*" ], "@eddystone-firebase-helpers/*": [ "src/helpers/fire ...

Angular 4 enum string mapping reversed

Here is an example of a string enum: export enum TokenLength { SIX = '6', EIGHT = '8', } I am trying to retrieve the string value 'SIX' or 'EIGHT' by reverse mapping this enum. I have attempted various methods: ...

Debugging TypeScript on a Linux environment

Approximately one year ago, there was a discussion regarding this. I am curious to know the current situation in terms of coding and debugging TypeScript on Linux. The Atom TypeScript plugin appears promising, but I have not come across any information ab ...

Troubleshooting issue with React and Material UI Table pagination display

Issue with Material UI Table Display When Changing Pages When receiving an array of Artist Objects through props to create a checklist table, I encounter some display issues. The table works fine initially, but when changing pages or sorting, more rows th ...

When initiating an Ionic project, you may notice a repeated message in the terminal saying, "[INFO] Waiting for connectivity with npm..."

I'm in the process of setting up a new Ionic project along with all the necessary dependencies. However, whenever I try to execute the command "ionic serve," I keep getting stuck at the continuous display of the message "[INFO] Waiting for connectivit ...

Utilize a personalized useFetch hook in React.js to transmit a POST request and obtain a response

I recently came across a great resource on this website that provided the logic for a useFetch hook. My goal is simple - I want to send a post request and then map the response into a specific type. While this seems like it should be straightforward, I&apo ...

Error: Unable to load the parser '@typescript-eslint/parser' as specified in the configuration file '.eslintrc.json' for eslint-config-next/core-web-vitals

When starting a new Next.js application with the specific configuration below: ✔ What name do you want to give your project? … app ✔ Do you want to use TypeScript? … No / [Yes] ✔ Do you want to use ESLint? … No / [Yes] ✔ Do you want to use T ...

Implementing a 12-month display using material-ui components

Just starting out with ReactJs, TypeScript, and material-ui. Looking to display something similar to this design: https://i.stack.imgur.com/zIgUH.png Wondering if it's achievable with material-ui. If not, any suggestions for alternatives? Appreciate ...

Transforming a mongodb operation into an asynchronous function using await and async syntax

After calling the function to retrieve data from MongoDB, an undefined error occurs. It is suspected that converting the function to an async/await function may resolve this issue. However, there is uncertainty on how to make this conversion without disrup ...

What led the Typescript Team to decide against making === the default option?

Given that Typescript is known for its type safety, it can seem odd that the == operator still exists. Is there a specific rationale behind this decision? ...

Utilize toggle functionality for page rotation with rxjs in Angular framework

Managing a project that involves a container holding multiple cards across different pages can be overwhelming. To address this, the screen automatically rotates to the next page after a set time interval or when the user presses the space bar. To enhance ...

Discovering subtype relationships in JSON with TypeScript

Consider the scenario where there are parent and child typescript objects: class Parent { private parentField: string; } class Child extends Parent { private childField: string; } Suppose you receive a list of JSON objects for both types via a R ...

What is the best way to assign a variable with the type (x:number)=>{y:number,z:number}?

I am trying to initialize a variable called foo, but my current code is not compiling successfully. let foo: (x: number) => {y:number,z: number} = (x) => {x+1, x+2}; This results in the following error: Left side of comma operator is unused and ha ...

Unlocking 'this' Within a Promise

I seem to have an issue with the 'this' reference in the typescript function provided. It is not correctly resolving to the instance of EmailValidator as expected. How can I fix this so that it points to the correct instance of EmailVaildator, al ...

After compilation, any variables declared within a module remain undefined

I have declared the following files app.types.ts /// <reference path="../../typings/tsd.d.ts"/> module App{ export var Module = "website"; //---------------Controller Base Types--------------- export interface IScope extends ng.ISco ...

Using TypeORM's QueryBuilder to select a random record with a nested relation

Imagine a scenario where I have the following entities: User @Entity('user', { synchronize: true }) export class UserEntity { @PrimaryGeneratedColumn('uuid') id: string; @Column() firstName: string; @Column() lastName: s ...