В данной статье мы рассмотрим как создать условия типов или type guards
на примере redux reducers
, эти принципы также могут применяться к любой функции.
У нас есть todoReducer
, который принимает state
. Он имеет тип Todo[]
и значение по умолчанию, а также action
с типом Action
.
import { Todo, Action, ActionTypes } from "../actions";
export const todosReducers = (state: Todo[] = [], action: Action ) => {
switch (action.type) {
case ActionTypes.fetchTodos:
return action.payload;
case ActionTypes.deleteTodo:
default:
return state;
}
}
export interface Todo {
id: number;
title: string;
completed: boolean;
}
export interface FetchTodosAction {
type: ActionTypes.fetchTodos;
payload: Todo[];
}
export interface DeleteTodoAction {
type: ActionTypes.deleteTodo;
payload: number;
}
export enum ActionTypes {
fetchTodos,
deleteTodo
}
export type Action = FetchTodosAction | DeleteTodoAction;
Action
это type
; который является объединением интерфейсов FetchTodosAction
и DeleteTodoAction
.
Каждое action
в redux
должно иметь как минимум type
. В нашем случае мы предоставили type
в виде enum ActionTypes
, который имеет fetchTodos
и deleteTodo
.
Во всех этих различных случаях (cases), оператор switch
действует как условия типов (type guards). Условия типов (type guards
) уменьшают количество различных случаев (cases) внутри нашего объединения типов (type Action
).
Поэтому, если мы наведём курсором мыши на action
внутри case ActionTypes.deleteTodo:
, то из-за этого оператора, typescript с абсолютной уверенностью знает, что action
будет иметь тип DeleteTodoAction
. И причина этого в том, что мы доберемся до action
только в том случае, если свойство action.type
будет равно deleteTodo
.
import { Todo, Action, ActionTypes } from "../actions";
export const todosReducers = (state: Todo[] = [], action: Action ) => {
switch (action.type) {
case ActionTypes.fetchTodos:
return action.payload;
case ActionTypes.deleteTodo:
action; // action: DeleteTodoAction
default:
return state;
}
}
Вот настоящая магия redux
и typescript
вместе взятых.
Всё сводится к настройке псевдонима типа действия type Action
и перечисления типов действий enum ActionTypes
путем настройки этих двух вещей вместе с оператором switch
.
export interface FetchTodosAction {
type: ActionTypes.fetchTodos;
payload: Todo[];
}
export interface DeleteTodoAction {
type: ActionTypes.deleteTodo;
payload: number;
}
export enum ActionTypes {
fetchTodos,
deleteTodo
}
export type Action = FetchTodosAction | DeleteTodoAction;
В каждом из разных случаев мы точно знаем, с каким типом объекта мы работаем внутри оператора case
. Мы знаем с абсолютной уверенностью, что свойство action
внутри ActionTypes.deleteTodo
будет равно DeleteTodoAction
.
export const todosReducers = (state: Todo[] = [], action: Action ) => {
switch (action.type) {
case ActionTypes.fetchTodos:
return action.payload;
case ActionTypes.deleteTodo:
action; // action: DeleteTodoAction
default:
return state;
}
}
И абсолютно точно знаем то, что payload
будет числом, представляющим id
, для того поля, которое хотим удалить.
export const todosReducers = (state: Todo[] = [], action: Action ) => {
switch (action.type) {
case ActionTypes.fetchTodos:
return action.payload;
case ActionTypes.deleteTodo:
action.payload; // DeleteTodoAction.payload: number
default:
return state;
}
}
Вот так мы действительно получаем отличную функциональность или сотрудничество между TypeScript и Redux.
Давай добавим логику для удаления задачи. Для этого мы вернем state
, отфильтруем все задачи и оставим только те, где идентификатор задачи не равен action.payload
. Именно так мы удалим задачу, которая соответствует идентификатору.
export const todosReducers = (state: Todo[] = [], action: Action ) => {
switch (action.type) {
case ActionTypes.fetchTodos:
return action.payload;
case ActionTypes.deleteTodo:
return state.filter((todo: Todo) => todo.id !== action.payload);
default:
return state;
}
}
Пример кода можно посмотреть здесь.