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

Incorporating a particular JavaScript library into Angular 4 (in case the library doesn't have a variable export)

I am attempting to display the difference between two JSON objects in an Angular 4 view. I have been using a library called angular-object-diff, which was originally created for AngularJS. You can view a demo of this library here: Link I have trie ...

What could be causing the Uncaught Error to persist even after using .catch()?

Check out this code snippet: function pause(ms:number) { return new Promise((resolve:any,reject:any) => setTimeout(resolve,ms)) } async function throwError(): Promise<void> { await pause(2000) console.log("error throw") throw new ...

Indicate the location of tsconfig.json file when setting up Cypress

Having trouble integrating Cypress with Typescript? I've encountered an issue where Cypress is unable to locate the tsconfig.json file I created for it. My preference is to organize my project with a custom directory structure, keeping configuration f ...

Guide on creating a zodiac validator that specifically handles properties with inferred types of number or undefined

There are some predefined definitions for an API (with types generated using protocol buffers). I prefer not to modify these. One of the types, which we'll refer to as SomeInterfaceOutOfMyControl, includes a property that is a union type of undefined ...

Step-by-step guide to developing an Angular 2+ component and publishing it on npm

I need assistance with creating an AngularX (2+) component and getting it published on npm. My objective is to publish a modal component I developed in my current Angular App, though currently, I am focusing on creating a <hello-world> component. It ...

How can one pass a generic tuple as an argument and then return a generic that holds the specific types within the tuple?

With typescript 4 now released, I was hoping things would be easier but I still haven't figured out how to achieve this. My goal is to create a function that accepts a tuple containing a specific Generic and returns a Generic containing the values. i ...

Unable to initiate ngModelChange event during deep cloning of value

I've been struggling to calculate the sum of row values, with no success. My suspicion is that the issue lies in how I am deep cloning the row values array when creating the row. const gblRowVal1 = new GridRowValues(1, this.color, this.headList ...

How can I dynamically reference two template HTML files (one for mobile and one for desktop) within a single component in Angular 6?

Here is the approach I have taken. Organizational structure mobile-view.component.html <p> This content is for mobile view </p> desktop-view.component.html <p> This content is for desktop view </p> mobile.component.ts import ...

Updating props in a recursive Vue 3 component proves to be a challenging task

I am facing an issue with two recursive components. The first component acts as a wrapper for the elements, while the second component represents the individual element. Wrapper Component <template> <div class="filter-tree"> &l ...

What is the best way to access data in a node.js application from IONIC typescript via a REST API?

Here is the structure of my service.ts: import { Injectable } from '@angular/core'; import {Http, Headers} from '@angular/http'; import 'rxjs/add/operator/map'; /* Generated class for the PeopleSearch provider. See http ...

Tips for preventing duplicate Java Script code within if statements

In my function, there are various statements to check the visibility of fields: isFieldVisible(node: any, field: DocumentField): boolean { if (field.tag === 'ADDR_KOMU') { let field = this.dfs_look(node.children, 'ADDR_A ...

Explore all user-defined properties of a specified type using the TypeScript Compiler API

Consider the following model structure: interface Address{ country: string; } interface Author{ authorId: number; authorName:string; address: Address; } interface Book{ bookId:string; title: string; author : Author; } I want to iterate th ...

Unable to utilize a generic model in mongoose due to the error: The argument 'x' is not compatible with the parameter type MongooseFilterQuery

Attempting to include a generic Mongoose model as a parameter in a function is my current challenge. import mongoose, { Document, Model, Schema } from 'mongoose'; interface User { name: string; age: number; favouriteAnimal: string; ...

The Angular tag <mat-expansion-panel-header> fails to load

Every time I incorporate the mat-expansion-panel-header tag in my HTML, an error pops up in the console. Referencing the basic expansion panel example from here. ERROR TypeError: Cannot read property 'pipe' of undefined at new MatExpansionPanel ...

Enhance the glowing spinner in a react typescript application

Recently, I transformed an html/css spinner into a react component. However, it seems to be slowing down other client-side processes significantly. You can see the original design on the left and the spinning version on the right in the image below: This ...

Join the Observable in Angular2 Newsletter for the latest updates and tips

One of my functions stores the previous URL address. prevId () { let name, id, lat, lng; this.router.events .filter(event => event instanceof NavigationEnd) .subscribe(e => { console.log('prev:', this.previo ...

Utilizing the spread operator in Typescript to combine multiple Maps into a fresh Map leads to an instance of a clear Object

Check out the code below: let m1 = new Map<string, PolicyDocument>([ [ "key1", new PolicyDocument({ statements: [ new PolicyStatement({ actions: [&q ...

What is the correct syntax for declaring a variable within a switch statement in TypeScript?

How can I properly use a switch statement in TypeScript to assign a new variable a value? For example: let name: string switch(index) { case 0: name = "cat" case 1: name = "dog" .... } I keep getting the err ...

Utilizing the axios create method: troubleshooting and best practices

I am attempting to use the axios library in my Next.js app (written in TypeScript) to access a public API for retrieving IP addresses from . In my index.ts file, I have the following code: import axios from "axios"; export const ipApi = axios.cr ...

What is the reason behind the ability to assign any single parameter function to the type `(val: never) => void` in TypeScript?

Take a look at the code snippet below interface Fn { (val: never): void } const fn1: Fn = () => {} const fn2: Fn = (val: number) => {} const fn3: Fn = (val: { canBeAnyThing: string }) => {} Despite the lack of errors, I find it puzzling. For ...