Ensure that the output of a function aligns with the specified data type defined in its input parameters

In the code snippet provided below, there is an attempt to strictly enforce return types based on the returnType property of the action argument. The goal is to ensure that the return type matches the specific returnType for each individual action, rather than just any generic returnType. Please refer to the Typescript Playground link for a better understanding.

// Sample code from a library
export declare type ActionCreator<T extends string = string> = (
  ...args: any[]
) => {
  type: T;
};
export declare type ActionCreatorMap<T> = { [K in keyof T]: ActionType<T[K]> };
export declare type ActionType<
  ActionCreatorOrMap
> = ActionCreatorOrMap extends ActionCreator
  ? ReturnType<ActionCreatorOrMap>
  : ActionCreatorOrMap extends object
    ? ActionCreatorMap<ActionCreatorOrMap>[keyof ActionCreatorOrMap]
    : never;

// Custom implementation starts here:
type GameActionTypes = "type1" | "type2";

type GameplayAction<T extends string, P, R> = P extends void
  ? { type: T; returnType: R }
  : { type: T; payload: P; returnType: R };

function action<R = void>() {
  return function<T extends GameActionTypes, P = undefined>(
    type: T,
    payload?: P
  ): GameplayAction<T, P, R> {
    return { type, payload } as any;
  };
}

const action1 = () => action()("type1", { a: 1, b: 2 });
const action2 = () => action<{ foo: "bar" }>()("type2", { c: 3, e: 4 });

type gameActions = typeof action1 | typeof action2;

// Narrow down a union by a specified tag
export type FindByTag<Union, Tag> = Union extends Tag ? Union : never;

// These cases work properly
type TEST1 = FindByTag<ActionType<gameActions>, { type: "type1" }>;
type TEST2 = FindByTag<ActionType<gameActions>, { type: "type2" }>;

export function executeAction<T extends GameActionTypes>(
  action: ActionType<gameActions>
): FindByTag<ActionType<gameActions>, { type: T }>["returnType"] {
  if (action.type === "type1") {
    // The enforced return type is `void`
    return;
  } else if (action.type === "type2") {
    //////////////// This should result in failure!!!
    // Expected return type: {foo: "bar"}
    return;
  }
}

Answer №1

When working with TypeScript, type parameters are not narrowed by type guards according to microsoft/TypeScript#24085. This means that while the check

action.type === "type1"
might narrow down action.type, it does not narrow down T, resulting in a return type like the union type void | {foo: "bar"}.

To work around this issue, one approach is to manually assert the return type in each clause where type guarding is used:

type Ret<T extends GameActionTypes> = 
  FindByTag<ActionType<gameActions>, {type: T}>["returnType"];

export function executeAction<T extends GameActionTypes>(
  action: ActionType<gameActions>
): Ret<T> {
  if (action.type === "type1") {
    type R = Ret<typeof action.type>;
    return undefined as R; // okay
  } else if (action.type === "type2") {
    type R = Ret<typeof action.type>;
    return undefined as R; // error
  }
}

It's important to note that the local type alias R differs in each guarded clause, leading to a successful assertion in one scenario and a failure in another. At present, there may not be a straightforward type-safe solution beyond this method.


UPDATE

Upon further examination, it was realized that the action argument was not generic, posing challenges with inferring the correct return value inside the implementation, as well as when calling the function itself. To address this, the argument must also be a generic type for better results:

export function executeAction<A extends ActionType<gameActions>>(
  action: A
): A["returnType"] {
  const actionUnion: ActionType<gameActions> = action; // remove generic
  if (actionUnion.type === "type1") {
    type R = Ret<typeof actionUnion.type>
    return undefined as R;
  } else if (action.type === "type2") {
    type R = Ret<typeof actionUnion.type>
    return undefined as R;
  }
}

By assigning the action type to A and specifying the return value as A['returnType'], the caller can now easily understand and expect the desired behavior of the function:

declare const t1: TEST1;
const ret1 = executeAction(t1); // void
declare const t2: TEST2;
const ret2 = executeAction(t2); // {foo: "bar"}

The function's implementation had to be adjusted accordingly, with the generic being A representing the type of the action rather than

T</code representing the type of the action's <code>type
property. Although narrowing remains challenging, assigning action to a non-generic variable
actionUnion</code allows for successful use of <code>return undefined as Ret<typeof actionUnion.type>
for narrowing purposes.

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

Retrieve functions with varying signatures from another function with precise typing requirements

My code includes a dispatch function that takes a parameter and then returns a different function based on the parameter value. Here is a simplified version: type Choice = 'start' | 'end'; function dispatch(choice: Choice) { switch ...

Exploring nested traversal within a list of DOM queries in Angular

During my development process, I faced a unique challenge that involved parent and child checkboxes at the same level. The desired behavior was to have children checkboxes automatically checked when the parent is checked, and vice versa. Although my HTML a ...

Error: Import statement is not allowed outside a module - Issue with Jest, Typescript in a React environment

Whenever I attempt to execute 'npm test', a troubling error arises. The command in my package.json file is as follows: "test": "jest --config ./config/jest/jest.config.ts", SyntaxError: Cannot use import statement outside a module 1 | import a ...

What are the steps to incorporate a 3D scene into a React website?

Can I get some advice on how to create a React web application using TypeScript? I want to be able to click a button and have it show a new page with a scene of a town. What is the best way to achieve this in my React project? I've heard about using R ...

When using vs code, the autoimport feature tends to offer up confusing import suggestions for rxjs operators and the CanActivate interface

While working on my Angular service, I utilized the rxjs map and switchMap operators. When prompted by VS Code to choose between two import statements for switchMap, I opted for the first one without noticing any major differences. However, this decision l ...

What is the best way to verify the presence of a value in an SQL column?

I need to check if a value exists in a column. If the value already exists, I do not want to insert it into the table. However, if it does not exist, then I want to add new data. Unfortunately, my attempted solution hasn't been successful. You can fi ...

How to address duplicate array objects when using splice in Angular 4

For the purpose of this question, I have simplified some code. this.getDataRuleList.splice(this.count, 1, dataRuleData); console.log(this.getDataRuleList); this.count += 1; The getDataRuleList function is responsible for returning an array of ...

Is it possible to target a specific element using Angular2's HostListener feature? Can we target elements based on their class name?"

Is there a way in Angular2 to target a specific element within the HostListener decorator? @HostListener('dragstart', ['$event']) onDragStart(ev:Event) { console.log(ev); } @HostListener('document: dragstart' ...

I possess a JSON array object and need to identify and extract the array objects that contain a specific child node

const jsonArray = { "squadName": "Super hero squad", "homeTown": "Metro City", "formed": 2016, "secretBase": "Super tower", "active": true, "members": [ { "name": "Molecule Man", "age": 29, "secretIdent ...

Can you explain the purpose of the "=" symbol in the function definition of "export const useAppDispatch: () => AppDispatch = useDispatch" in TypeScript?

Recently, while working on a React app that utilizes react-redux and react-toolkit, I encountered some TypeScript syntax that left me puzzled. export type RootState = ReturnType<typeof store.getState> export type AppDispatch = typeof store.dispatch e ...

The RxJs Observer connected to a websocket only triggers for a single subscriber

Currently, I am encapsulating a websocket within an RxJS observable in the following manner: this.wsObserver = Observable.create(observer=>{ this.websocket.onmessage = (evt) => { console.info("ws.onmessage: " + evt); ...

The error message "TypeError: render is not a function" is encountered when attempting to call and display information

I am currently working on a movie app using React JS and I encountered an error while trying to render a component based on API data. TypeError: Render is not a function The code works perfectly fine for TvListProvider, but I'm facing this error wi ...

What is the process of refactoring TypeScript and React project expressions?

Currently, I am utilizing TypeScript in a React project but encountering type errors. Please assist me by examining my code. I need help with refactoring this code. I believe the issue lies in the expression that uses the args of the useTabs function. ...

Encountering an issue post-upgrade with Angular 7 project

Currently, I am facing an issue with upgrading a project from Angular 6 to version 7. Despite following multiple online tutorials and successfully completing the upgrade process, I encountered an error when running the 'ng serve' command: ERROR ...

This browser does not recognize the tag <>. To render a React component, ensure its name starts with an uppercase letter

MyIcons.tsx export const ICONCAR = () => ( <span className="svg-icon svg-icon-primary svg-icon-2x"><svg xmlns="http://www.w3.org/2000/svg" xmlnsXlink="http://www.w3.org/1999/xlink" width="24px" height=&qu ...

Prevent Click Event on Angular Mat-Button

One of the challenges I'm facing involves a column with buttons within a mat-table. These buttons need to be enabled or disabled based on a value, which is working as intended. However, a new issue arises when a user clicks on a disabled button, resul ...

unable to modify the attributes of an object in Angular

I've been tasked with modifying an existing Angular project that includes a component where I have the following variable: public testRunDetails: ITestRunDetails[] = []; The variable testRunDetails is of type ITestRunDetails, which is defined as: exp ...

Step-by-step guide on importing `block-ui`, `spectrum-colorpicker`, and `sass.js` libraries in a TypeScript project

I have been utilizing various plugins such as block-ui, spectrum-colorpicker, sass.js, etc., in my Angular 2 project written in TypeScript. Currently, I am loading these plugins directly in the index.html file. However, I would like to import them and onl ...

Confirm button title by verifying part of the label that contains a space

I'm facing an issue with clicking a button using the following code: await page.getByRole('button', { name: '3 Employees' }).click(); The problem is that the button's name fluctuates based on the number of employees, causing ...

Is there a way to trigger an error event in Jest using TypeScript?

As an illustration, let's take a look at how I'm utilizing Archiver: archive.on('error', err => { if (typeof callback === 'function') { callback.call(this, err); } else { throw err; } }); It appears that the ...