What is the best way to implement a dispatch function in TypeScript?

Despite my expectations, this code does not pass typechecking. Is there a way to ensure it is well typed in Typescript?

const hh = {
  a: (_: { type: 'a' }) => '',
  b: (_: { type: 'b' }) => '',
} as const;

export function dispatch<
  Arg extends Parameters<(typeof hh)[keyof typeof hh]>[0],
>(arg: Arg) {
  const h = hh[arg.type];
  h(arg);
}

The purpose of the dispatch() function is to accept a valid argument for one of the functions stored in hh and invoke that function with the given argument.

Answer №1

The compiler is having difficulty handling the concept of "correlated union types" as discussed in ms/TS#30581. However, there is a suggested workaround provided officially in ms/TS#47109, which involves using a map type that acts as the single source of truth for functions and dispatch arguments.

Let's define this map type:

type ActionMap = {
  a: {
    payload?: {};
  };
  b: {
    payload?: {};
  };
};

Assuming that you are working with redux actions, I have incorporated an optional payload property.

Now, we can type hh utilizing this type along with mapped types:

const hh: {
  [K in keyof ActionMap]: (action: ActionMap[K] & { type: K }) => string;
} = {
  a: (action) => '',
  b: (action) => '',
};

We will also create another type for the dispatch arguments, where we return a default union along with specific arguments based on the generic argument representing the dispatched action:

type DispatchArgs<T extends keyof ActionMap = keyof ActionMap> = {
  [K in T]: { type: K } & ActionMap[K];
}[T];

Example of usage:

export function dispatch<ActionType extends keyof ActionMap>(
  arg: DispatchArgs<ActionType>,
) {
  const h = hh[arg.type];

  h(arg); // no error
}

Link to Playground

Answer №2

TLDR: https://tsplay.dev/we8MYW

To simplify the implementation of the map, I created a function that generates a dispatch function tailored for a specific key in the map. This approach allowed us to validate the key against the set rules before proceeding.

function createDispatch(hh: {[key in string]: (_: { type: key }) => any}) {
  return dispatch(arg: { type: key }) {
    // ...
  }
}

I then created a Generic type that takes a union of keys and outputs an object where keys and values are identical.

type ExactValue<Of extends string> = {
  [K in Of]: K;
};

type TestExactValue = ExactValue<"a" | "b"> // { a: "a", b: "b" }
type HH<O> = { readonly [key in keyof O]: (_: { type: O[key]}) => any }

type TestHH = HH<TestExactValue> // { a: (_: { type: "a" }) => any, b: (_: { type: "b" }) => any }
function createDispatcher<Key extends keyof typeof hh, O extends ExactValue<Key>, WW extends { readonly [key in keyof O]: (_: { type: O[key]}) => any }>(hh: WW) {
type ExactValue<Of extends string> = {
  [K in Of]: K;
};


function createDispatcher<Key extends keyof typeof hh, O extends ExactValue<Key>, WW extends { readonly [key in keyof O]: (_: { type: O[key]}) => any }>(hh: WW) {
  return function dispatch<MyKey extends Key>(arg: { type: Key } & { type: MyKey }) {
    const h = hh[arg.type]
    return h(arg as any) as ReturnType<typeof h>
  }
}

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

The property 'filter' is not recognized on the 'Object' type. An attempt to filter the response was made

Trying to fetch data from a JSON file that matches the player's name in the URL, such as localhost:4200/players/Febiven, should only retrieve information about Febiven. The code is written in Angular 6. The current code snippet is as follows: player ...

Constructor polymorphism in TypeScript allows for the creation of multiple constructor signatures

Consider this straightforward hierarchy of classes : class Vehicle {} class Car extends Vehicle { wheels: Number constructor(wheels: Number) { super() this.wheels = wheels } } I am looking to store a constructor type that ext ...

The try-catch statement in Typescript is generating an inconsistent return error

I've encountered an issue with a TypeScript function that is flagging inconsistent return error. The function includes a try block that returns a value and a catch block that throws an error, resulting in the inconsistency. I am struggling to find a w ...

TypeScript test framework for testing API calls in VS Code extensions

My VS Code extension in TypeScript uses the Axios library for API calls. I have written tests using the Mocha framework, which are run locally and through Github Actions. Recently, I integrated code coverage reporting with `c8` and I am looking to enhanc ...

typescript function intersection types

Encountering challenges with TypeScript, I came across the following simple example: type g = 1 & 2 // never type h = ((x: 1) => 0) & ((x: 2) => 0) // why h not never type i = ((x: 1 & 2) => 0)// why x not never The puzzling part is w ...

Issue encountered while deploying Next.js application on vercel using the replaceAll function

Encountering an error during deployment of a next.js app to Vercel, although local builds are functioning normally. The issue seems to be related to the [replaceAll][1] function The error message received is as follows: Error occurred prerendering page &q ...

The pivotal Angular universal service

In my application, I have the need to store global variables that are specific to each user. To achieve this, I created a Service that allows access to these variables from any component. However, I am wondering if there is a way to share this service t ...

How to access the audio element in Angular using ViewChild: Can it be treated as an

My goal is to include an audio element inside a component. Initially, I approached this by using traditional methods: $player: HTMLAudioElement; ... ngOnInit() { this.$player = document.getElementById('stream') } However, I wanted to follow T ...

Always deemed non-assignable but still recognized as a universal type?

I'm curious about why the never type is allowed as input in generic's extended types. For example: type Pluralize<A extends string> = `${A}s` type Working = Pluralize<'language'> // 'languages' -> Works as e ...

The combination of TypeScript decorators and Reflect metadata is a powerful tool for

Utilizing the property decorator Field, which adds its key to a fields Reflect metadata property: export function Field(): PropertyDecorator { return (target, key) => { const fields = Reflect.getMetadata('fields', target) || []; ...

Tips for utilizing mergeWith with Subjects in an Angular application

Objective: Creating a Combined Filter with 3 Inputs to refine a list Conditions: Users are not required to complete all filters before submission The first submit occurs when the user inputs data Inputs are combined after more than one is provided Appro ...

Guide on integrating react-tether with react-dom createPortal for custom styling of tethered components based on their target components

Within a Component, I am rendering buttons each with its own tooltip. The challenge is to make the tooltip appear upon hovering over the button since the tooltip may contain more than just text and cannot be solely created with CSS. The solution involves ...

Set up your Typescript project to transpile code from ES6 to ES5 by utilizing Bable

Embarking on a new project, I am eager to implement the Async and Await capabilities recently introduced for TypeScript. Unfortunately, these features are currently only compatible with ES6. Is there a way to configure Visual Studio (2015 Update 1) to co ...

Typescript struggles to identify properties that have no business being there

In my function for formatting data, the "values" contained within this data are clearly defined. However, TypeScript is failing to recognize new properties that are invalid when mapping these values. The issue can be best understood by looking at the code ...

Unable to run `create-react-app` with the `--template typescript` option

Every time I try to execute the following command: npx create-react-app my-app --template typescript only a JavaScript project is created, without any .tsx files. After consulting the CRA's TypeScript guide, it appears that the command requires Node ...

Using command line arguments in a Tauri project with a Next.js frontend

I am utilizing Tauri.JS in conjunction with Next.js. In this scenario, I need to execute the console command: npm run tauri dev --<argument name>=<some value>. Afterwards, I should be able to access the value of the argument in my JavaScript ...

The array containing numbers or undefined values cannot be assigned to an array containing only numbers

Currently facing an issue with TypeScript and types. I have an array of IDs obtained from checkboxes, which may also be empty. An example of values returned from the submit() function: const responseFromSubmit = { 1: { id: "1", value: "true" }, 2: ...

RXJS expand keeps on recursing indefinitely

After successfully uploading a file to Firebase, I implemented a recursive function to listen for GCP trigger logs. Everything seems to be working well, but I'm facing an issue where the recursive expand function never exits. Despite checking the val ...

Update the style dynamically in Angular using an ngFor directive and an array of data

Is there a way to dynamically set the width using data from an array in Angular? The usual approach doesn't seem to work. How can I solve this issue? <div *ngFor="let item of products"> <div [style.width.px]="item.size" class="Holiday"&g ...

RxJS emits an array of strings with a one second interval between each emission

Currently, my code is set up to transform an Observable<string[]> into an Observable<string>, emitting the values one second apart from each other. It's like a message ticker on a website. Here's how it works at the moment: const ...