(Heads up for Typescript beginners)
I'm working on a reusable reducer function that takes a state and an action, but is designed to only accept state objects with a specific type at a particular key. This key is passed as a parameter to the function. If the passed state object doesn't have the specified key, I want the compiler to throw an error.
While I've managed to get this working, I'm not happy with how the compiler handles the error messaging. It doesn't directly point out that the expected property is missing, instead throwing other errors which I'll explain below.
Types
// FetchAction definition here...
export type FetchState = {
status: FetchAction,
timestamp: Date
} | null
export type LoginState = {
token: string | null,
fetching: FetchState
};
Base Reducer
const initialState: LoginState = {
token: null,
fetching: null
}
const loginReducer: Reducer<LoginState> = (state = initialState, action) => {
//...other operations
return fetchingReducer(state, action, 'fetching');
}
Method 1
type FetchContainingState<S, K extends keyof S> = {
[F in keyof S]: F extends K ? FetchState : S[F];
};
export const fetchingReducer = <S extends FetchContainingState<S, K>, K extends keyof S>(state: S, action: Action, key: K): S => {
// implementation
}
This solution works well. When I make a mistake in the function call like so:
return fetchingReducer(state, action, 'fetchin');
(misspelling 'fetching'
), I receive this error message:
Argument of type 'LoginState' is not assignable to parameter of type 'FetchContainingState'. Types of property 'token' are incompatible. Type 'string | null' is not assignable to type 'FetchState'. Type 'string' is not assignable to type 'FetchState'.
It's good that it catches the error, but it only mentions the property like "token", without explicitly indicating which property it expected but did not find.
Method 2
type EnsureFetchState<S, K extends keyof S> = S[K] extends FetchState ? S : never;
export const fetchingReducer = <S, K extends keyof S>(state: EnsureFetchState<S, K>, action: Action, key: K): S => {
// implementation
}
This approach also works, and when I mistype the call as
return fetchingReducer(state, action, 'fetchin');
(instead of "fetching"
), I get:
Argument of type 'LoginState' is not assignable to parameter of type 'never'.
Shorter response, but even less descriptive of the issue. It provides minimal information on what might be wrong with the arguments.
Conclusion
In Method 1, I used a Mapped Type, while in Method 2, I employed a Conditional Type to check if the values passed for state
and key
meet our criteria. However, both approaches generate error messages that don't clearly identify the actual problem.
As someone new to more advanced TypeScript types, there might be a simpler way or concept that I'm missing. Hopefully! But overall, my main question is: How can we perform this type checking on an object with a dynamic key more effectively, generating clearer error messages?