Using TypeScript generics to create reusable type definitions for reducers

I have a common reducer function that needs to be properly typed.
Here's what I have come up with:

export interface WithInvalidRows<T extends { [K in keyof T]: InvalidCell[] }> {
  invalidRows: T;
}

interface AddPayload<S extends WithInvalidRows<S['invalidRows']>> {
  cell: InvalidCell;
  screenKey: keyof S['invalidRows'];
}

const addInvalidCell = <S extends WithInvalidRows<S['invalidRows']>>(
  state: S, 
  { cell, screenKey }: AddPayload<S>
) => {
  const rows = state.invalidRows[screenKey];
  const index = R.findIndex((item) => item.id === cell.id, rows);

  ...
};

However, I am encountering the error message

Type 'S["invalidRows"]' does not satisfy the constraint '{ [K in keyof S["invalidRows"]]: InvalidCell[]; }'.

I'm confused about why S['invalidRows'], which should ultimately equal
{ [K in keyof T]: InvalidCell[] }
, is not assignable in this context.


Appendix:

By using the type

type RowsExtend<T> = Record<keyof T, InvalidCell[]>;
export interface WithInvalidRows<T extends RowsExtend<T> = any> {
  invalidRows: RowsExtend<T>;
}

I now encounter an error

TS2589: Type instantiation is excessively deep and possibly infinite.
at this point:

const reducer = createReducer(
  on(ImportScreenActions.addInvalidEntityRowImportScreen, addInvalidCell),
  ...
);

The types are imported from @ngrx:

export declare type ActionCreator<T extends string = string, C extends Creator = Creator> = C & TypedAction<T>;
export declare type ActionType<A> = A extends ActionCreator<infer T, infer C> ? ReturnType<C> & {
    type: T;
} : never;
export interface OnReducer<S, C extends ActionCreator[]> {
    (state: S, action: ActionType<C[number]>): S;
}
export declare function on<C1 extends ActionCreator, S>(creator1: C1, reducer: OnReducer<S, [C1]>): On<S>;

Answer №1

Identifying the cause of the error proved to be quite challenging. Although it seemed fairly evident that the issue was related to the "recursive" generic type, such as

S extends WithInvalidRows<S['invalidRows']>
, obtaining more specific information was elusive. I opted for an experimental approach of tinkering with the code to gain some insight.


The initial successful modification to your code is as follows:

interface InvalidCell {
    id: number;
}

export interface WithInvalidRows<T extends { [K in keyof T]: InvalidCell[] }> {
    invalidRows: T;
}

interface AddPayload<R extends { [K in keyof R]: InvalidCell[] }> {
    cell: InvalidCell;
    screenKey: keyof R;
}

const addInvalidCell = <
    R extends { [K in keyof R]: InvalidCell[] },
    S extends WithInvalidRows<R>
>(
    state: S,
    { cell, screenKey }: AddPayload<R>
) => {
    const rows = state.invalidRows[screenKey];
    const id: number = rows[0].id;
};

interface MyState extends WithInvalidRows<InvalidRowsScreen> {
    someExtraState: string;
}

interface InvalidRowsScreen {
    foo: InvalidCell[];
    bar: InvalidCell[];
    baz: InvalidCell[];
}

const myState: MyState = null!;
const invalidRows: InvalidRowsScreen = myState.invalidRows;
const someExtraState: string = myState.someExtraState

// Valid
addInvalidCell(myState, {
    cell: { id: 123 },
    screenKey: 'foo',
});
// Error occurs due to lack of 'invalid' field in myState
addInvalidCell(myState, {
    cell: { id: 123 },
    screenKey: 'invalid',
});

I had to alter the representation of the generic parameter in AddPayload and introduce this same generic parameter to addInvalidCell. TypeScript is able to handle both types gracefully, as demonstrated in the example function calls above.


After further experimentation, the error mysteriously disappeared when I made a slight adjustment to WithInvalidRows:

interface InvalidCell {
    id: number;
}

export interface WithInvalidRows<T extends { [K in keyof T]: InvalidCell[] }> {
    invalidRows: { [key in keyof T]: InvalidCell[] }; // originally just `T`
}

interface AddPayload<S extends WithInvalidRows<S['invalidRows']>> {
    cell: InvalidCell;
    screenKey: keyof S['invalidRows'];
}

const addInvalidCell = <S extends WithInvalidRows<<S['invalidRows']>>>(
    state: S,
    { cell, screenKey }: AddPayload<S>
) => {
    const rows = state.invalidRows[screenKey];
    const id: number = rows[0].id;
};

interface MyState extends WithInvalidRows<InvalidRowsScreen> {
    someExtraState: string;
}

interface InvalidRowsScreen {
    foo: InvalidCell[];
    bar: InvalidCell[];
    baz: InvalidCell[];
}

const myState: MyState = null!;

// Valid
addInvalidCell(myState, {
    cell: { id: 123 },
    screenKey: 'foo',
});
addInvalidCell(myState, {
    cell: { id: 123 },
    screenKey: 'invalid', // Error thrown for 'invalid' not being assignable to `keyof InvalidRowsScreen`
});

My explanation may not be definitive, but it appears that the verbose nature of the modified code allows TypeScript to handle the "recursive generic" more effectively.

By specifying

{ [key in keyof T]: InvalidCell[] }
for invalidRows, it essentially tells TypeScript "that generic parameter T? can take any form", enabling S to pass the type validation. Consequently, keyof T (equivalent to keyof S['invalidRows']) is computed without issues.

A similar solution preventing errors involving S['invalidRows'] could look like this:

interface WithInvalidRowsKeys<K extends keyof any> {
    invalidRows: { [key in K]: InvalidCell[] };
}

export type WithInvalidRows<T extends { [K in keyof T]: InvalidCell[] }> = WithInvalidRowsKeys<keyof T>;

Similar to the previous fix, we are not directly including invalidRows: T in testing WithInvalidRows. Initially, T undergoes type checking, followed by the utilization of keyof T to define the fields of WithInvalidRows.

These are merely theory-based assumptions derived from practical trial and error.

Answer №2

Check out the finalized pull request where the typescript team has made enhancements to the features of indexed access type in various ways.

Here are some key features that I would like to highlight, which are relevant to the current functionality.

Previously, when an indexed access T[K] was used on the target side of a type relationship, it would resolve to a union type of the selected properties by T[K]. Now, it resolves to an intersection type of these properties, providing soundness as opposed to a union type resolution.

If a type variable T with a constraint C undergoes an indexed access T[K] on the target side of a type relationship, index signatures in C will be disregarded. This adjustment ensures that a type argument for T does not necessarily need an index signature but must have matching type properties.

The constraint Record<string, XXX> does not guarantee that an argument possesses a string index signature, but rather ensures that its properties are assignable to type XXX.

In your specific situation,

export interface WithInvalidRows<T extends { [K in keyof T]: InvalidCell[] }> {
  invalidRows: T;
}
interface AddPayload<S extends WithInvalidRows<S["invalidRows"]>> {
    cell: InvalidCell;
    screenKey: keyof S["invalidRows"];
}

S["invalidRows"] represents the type of the invalidRows key of generic T,

S['invalidRows'] = T

Since we do not know the index signature of T, S['invalidRows'] cannot recognize it as an index type.

Hence, the only solution I can propose is assigning the index signature of generic T. As it is an interface and cannot be typecast, the type of invalidRows key must match the constraint type.

type RowsExtend<T> = Record<keyof T, InvalidCell[]>;
export interface WithInvalidRows<T extends RowsExtend<T>> {
    invalidRows: RowsExtend<T>;
}

Complete code including examples:

export interface InvalidCell {
    id: string;
    severity?: string;
}

type RowsExtend<T> = Record<keyof T, InvalidCell[]>;
export interface WithInvalidRows<T extends RowsExtend<T>> {
    invalidRows: RowsExtend<T>;
}

interface AddPayload<S extends WithInvalidRows<S["invalidRows"]>> {
    cell: InvalidCell;
    screenKey: keyof S["invalidRows"];
}

interface MyState extends WithInvalidRows<InvalidRowsScreen> {
    // additional states
}

interface InvalidRowsScreen {
    foo: InvalidCell[];
    bar: InvalidCell[];
    baz: InvalidCell[];
}
const addInvalidCell = <S extends WithInvalidRows<S["invalidRows"]>>(
    state: S,
    { cell, screenKey }: AddPayload<S>
) => {
    const rows = state.invalidRows[screenKey];
    const index = rows.findIndex(item => item.id === cell.id, rows);
};

const someState: MyState = {
    invalidRows: {
        foo: [{ id: "22" }],
        bar: [{ id: "123" }],
        baz: [{ id: "634" }],
    },
    // more states
};

addInvalidCell(someState, {
    cell: { id: "123" },
    screenKey: "foo",
});

addInvalidCell(someState, {
    cell: { id: "123" },
    screenKey: "invalid",
});

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

Leverage the power of InfiniteSWR in your project by integrating it seamlessly with

If you're looking for a comprehensive example of how to integrate useSWR hooks with axios and typescript, check out the official repository here. import useSWR, { SWRConfiguration, SWRResponse } from 'swr' import axios, { AxiosRequestConfig, ...

Creating a custom class to extend the Express.Session interface in TypeScript

I'm currently tackling a Typescript project that involves using npm packages. I am aiming to enhance the Express.Session interface with a new property. Here is an example Class: class Person { name: string; email: string; password: strin ...

Utilizing a library that solely enhances the functionality of the Array object

I have a library with type definitions structured like this: declare global { interface Array<T> { addRange<T>(elements: T[]): void; aggregate<U>(accumulator: (accum: U, value?: T, index?: number, list?: T[]) => an ...

Is there a way I can replace this for loop with the array.some function?

I am looking to update the filterOutEmails function in the following class to use array.some instead of the current code. export class UsertableComponent { dataSource: MatTableDataSource<TrialUser> createTableFromServer = (data: TrialUsers[], ...

Create a new function and assign it to "this" using the button inside ngFor loop

I am working with a li tag that has a *ngFor directive: <li *ngFor="let items of buttons"> <button (click)="newMap(items.id, $event)"> {{ items.name }} </button> </li> The buttons array looks like this: buttons = [ {nam ...

Using kotlinx.serialization to deserialize a JSON array into a sealed class

Stored as nested JSON arrays, my data is in rich text format. The plaintext of the string and annotations describing the formatting are stored in text tokens. At decode time, I aim to map the specific structure of these nested JSON arrays to a rich Kotlin ...

Encountering the error message 'array expected for services config' within my GitLab CI/CD pipeline

My goal is to set up a pipeline in GitLab for running WebdriverIO TypeScript and Cucumber framework tests. I am encountering an issue when trying to execute wdio.conf.ts in the pipeline, resulting in this error: GitLab pipeline error Below is a snippet of ...

What is the best way to replicate certain key-value pairs in an array of objects?

I am working with an array of objects. resources=[{name:'x', title:'xx',..},{name:'y',title:'yy',..}..] To populate my HTML tooltip, I am pushing all the titles from the resources array to a new array. dialogOkCli ...

Callback after completion of a for loop in Angular TypeScript

During the execution of my javascript async function, I encountered a situation where my printing code was running before the div creation. I needed to ensure that my print code would run only after the completion of the for loop, but I struggled to find a ...

Modify the title and go back dynamically in the document

I am currently working on a timer app where I want to dynamically change the document title. The app features a countdown timer, and during the countdown, I was able to display the timer in the document title successfully. However, once the countdown is co ...

Typescript and Visual Studio Code Issue: Module "myimage.png" Not Found

I am encountering an issue where VS Code is complaining about not being able to find a module when trying to import an image from an assets directory within my project. Despite the fact that the image import works fine, I keep receiving the error message: ...

Using React TypeScript: maximize the efficiency of sending multiple JSON objects to the frontend with the useData hook

Currently, I am experimenting with a customized useData hook based on the React Beginner course by CodingWithMosh. This modified hook contains multiple objects within it. In the original course content, all data was stored in the results JSON object. I am ...

Guide on assigning JSON response values to TypeScript variables in Angular 4

I'm just starting with Angular 4 and I'm attempting to retrieve a JSON value using http.post. The response I'm receiving is: {"status":"SUCCESS"} component onSubmit(value: any) { console.log("POST"); let url = `${this.posts_Url}`; t ...

TypeScript and Redux mapDispatchToProps are not in sync

Below is my React component written in TypeScript: import React from 'react'; import {connect, ConnectedProps} from 'react-redux'; import logo from './assets/logo.png'; // import { Counter } from './features/counter/Count ...

Why do I keep receiving a <prototype> object for each API request?

Currently, I am utilizing JSONPlaceholder in conjunction with Angular as part of my learning process. While I have been following the documentation meticulously and obtaining the correct output, there seems to be an additional element accompanying each obj ...

Issue TS2322: The type 'Observable<any>' cannot be matched with type 'NgIterable<any> | null | undefined'

Encountering an error while attempting to fetch data from the API. See the error image here. export class UserService { baseurl: string = "https://jsonplaceholder.typicode.com/"; constructor(private http: HttpClient) { } listUsers(){ //this ...

Deactivate the rows within an Office UI Fabric React DetailsList

I've been attempting to selectively disable mouse click events on specific rows within an OUIF DetailsList, but I'm facing some challenges. I initially tried overriding the onRenderRow function and setting CheckboxVisibility to none, but the row ...

What is the best way to store types in a TypeScript-based React/Next application?

I'm currently in the process of setting up a Next.js page in the following manner const Index: NextPage<PageProps> = (props) => { // additional code... Prior to this, I had defined my PageProps as follows: type PageProps = { pictures: pi ...

Try skipping ahead to the following spec file even if the initial spec has not been completed yet

I have encountered an issue when trying to execute two spec files for Angular.js. The problem arises when I attempt to run both files consecutively - the browser initially opens for angular.js, then switches to angularCal.js without executing angular.js ...

Hide the MaterialTopTabNavigator from view

Is there a way to hide the react native MaterialTopTabNavigator on the screen while still maintaining the swiping functionality between screens? I want the user to be able to swipe between screens but not see the tab navigator itself. const Tab = creat ...