It's quite difficult to pinpoint why it gives that error. While it was kind of obvious it had to do with the "recursive" generic type, e.g. S extends WithInvalidRows<S['invalidRows']>, getting more details was tricky. I went with the "let me play around with it and try some things out" approach to maybe shed some light.
A first working modification of your code is this:
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 that myState misses the field 'invalid'
addInvalidCell(myState, {
cell: { id: 123 },
screenKey: 'invalid',
});
I did have to change what the generic parameter of AddPayload represents and add this same generic parameter to addInvalidCell. TypeScript can still interfere both types, as seen in the example function calls above.
Then after some more fiddling, the error surprisingly disappears when I changed the WithInvalidRows just a bit:
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 that 'invalid' is not assignable to `keyof InvalidRowsScreen`
});
I'd be lying if I said that I know exactly why this fixes it. Somehow being extra verbose there makes TypeScript handle the "recursive generic" better.
I'd say it's because S extends WithInvalidRows<S['invalidRows']> tries to read the invalidRows: T field which it tries to read from the (passed) generic parameter which is actually S['invalidRows'], and somehow TypeScript can't compute that (still S-validation) type loop.
By using invalidRows: { [key in keyof T]: InvalidCell[] }, it's more like telling TypeScript "that generic parameter T? can be whatever you want", which allows S to pass the type validation. And since S is valid, keyof T which is keyof S['invalidRows'] can just be calculated fine.
A similar example that stops your error from making S['invalidRows'] error:
interface WithInvalidRowsKeys<K extends keyof any> {
invalidRows: { [key in K]: InvalidCell[] };
}
export type WithInvalidRows<T extends { [K in keyof T]: InvalidCell[] }> = WithInvalidRowsKeys<keyof T>;
Just like the previous solution, we're not directly making invalidRows: T part of verification of WithInvalidRows. T gets type checked, then keyof T is used to define WithInvalidRows's fields.
But this is all just my theory based on what did and didn't work.