То, как вы определили свою функцию typedAction
, работает нормально:
export function typedAction<T extends string>(type: T): { type: T };
export function typedAction<T extends string, P extends any>(
type: T,
payload: P
): { type: T; payload: P };
export function typedAction(type: string, payload?: any) {
return { type, payload };
}
Проблема, которую вы испытываете, заключается в разрушении действия в параметрах вашего редуктора:
export const tokenReducer = (
state: IState["token"] = null,
{ type, payload }: AppAction
): typeof state => {
// ...
};
Одна из трудностей с деструктуризацией и TypeScript заключается в том, что после этого тип переменных становится независимым друг от друга. Разбиение действия на { payload, type }
создает переменные type: 'LOGIN' | 'LOGOUT'
и payload: string | undefined
. Даже если вы позже уточните значение type
, как в вашем операторе switch, payload
все равно будет иметь тип string | undefined
; TypeScript не будет автоматически уточнять тип payload
в блоке case после уточнения type
; их наборы полностью независимы.
Таким образом, довольно уродливый хак, который вы можете использовать, заключается в том, чтобы не деструктировать:
export const tokenReducer = (
state: IState['token'] = null,
action: AppAction,
): typeof state => {
switch (action.type) {
case 'LOGIN':
return action.payload;
case 'LOGOUT':
return null;
default:
return state;
}
};
Это работает, потому что в вашем операторе switch он может улучшить action: AppAction
введите более специфичные c типы входа или выхода, так что action.payload
теперь сильно привязан к типу полезной нагрузки, указанному c для одного из этих действий.
Вот альтернативный шаблон для редукционных действий, которые я использую что вы можете найти более удобным на моей вилке , который позволяет вам наслаждаться мощью отображаемых типов для определения редукторов с меньшим количеством шаблонов. Во-первых, вы должны определить тип с сопоставлениями типа / полезной нагрузки и определить некоторые типы, вытекающие из этого:
export type ActionPayloads = {
LOGIN: string;
LOGOUT: void;
};
export type ActionType = keyof ActionPayloads;
export type Action<T extends ActionType> = {
type: T;
payload: ActionPayloads[T];
};
Ваши создатели действий теперь могут быть определены в терминах этой карты:
export function typedAction<T extends ActionType>(
type: T,
payload: ActionPayloads[T]
) {
return { type, payload };
}
Далее вы можете определить вспомогательную функцию для создания строго типизированного редуктора:
type ReducerMethods<State> = {
[K in ActionType]?: (state: State, payload: ActionPayloads[K]) => State
};
type Reducer<State> = (state: State, action: AppAction) => State;
function reducer<State>(
initialState: State,
methods: ReducerMethods<State>
): Reducer<State> {
return (state: State = initialState, action: AppAction) => {
const handler: any = methods[action.type];
return handler ? handler(state, action.payload) : state;
};
}
(я не нашел хорошего обходного пути для этого уродливого : any
приведения, но по крайней мере мы знаем логически что печатание является звуковым извне).
Теперь вы можете определять свои редукторы, таким образом, с помощью неявной типизации для ваших обработчиков действий:
type TokenState = string | null;
export const tokenReducer = reducer<TokenState>(null, {
LOGIN: (state, token) => token, // `token` is implicitly typed as `string`
LOGOUT: () => null // TS knows that the payload is `undefined`
});