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.