TypeError: Cannot destructure property 'toDos' of 'Object(...)(...)' as it is undefined

TypeError: Cannot destructure property 'toDos' of 'Object(...)(...)' as it is undefined

我是初学者,第一次学习ts。预先感谢您分享您的知识。 我正在列一个待办事项清单。我曾经反应来完成它。但是现在我是用react和typescript一起来完成代码的。

在我看来 'reducer' 工作不正常。我该如何操作? toDos, completed 都有错误。我的电脑根本不带这些东西。 如果你让我知道,我将不胜感激。这是 'App.tsx' 有表面错误的代码。

import React from "react";
import Add from "./Add";
import List from "./List";
import ToDo from "./ToDo";
import Title from "./Title";
import Progress from "./Progress";
import styled from "styled-components";
import { useTodosState } from '../context';

function App() {
  const { toDos, completed } = useTodosState();

  return (
    <Title>
      <Add />
      <Progress />
      <Lists>
        <List title={toDos.length !== 0 ? "To Dos" : ""}>
          {toDos.map((toDo: any) => (
            <ToDo key={toDo.id} id={toDo.id} text={toDo.text} isCompleted={false} />
          ))}
        </List>
        <List title={completed.length !== 0 ? "Completed" : ""}>
          {completed.map((toDo: any) => (
            <ToDo key={toDo.id} id={toDo.id} text
              {...toDo.text} isCompleted />
          ))}
        </List>
      </Lists>
    </Title>
  );
}

export default App;

这段代码是我认为有问题的'reducer.tsx'代码。

import { v4 as uuidv4 } from "uuid";
import { ADD, DEL, COMPLETE, UNCOMPLETE, EDIT } from "./actions";

export const initialState = {
  toDos: [],
  completed: [],
};

interface IReducer {
  state: any;
  action: any;
}

const Reducer = ({ state, action }: IReducer) => {
  switch (action) {
    case ADD:
      return {
        ...state,
        toDos: [...state.toDos, { text: action.payload, id: uuidv4() }],
      };
    case DEL:
      return {
        ...state,
        toDos: state.toDos.filter((toDo: { id: number; }) => toDo.id !== action.payload),
      };
    case COMPLETE:
      const target = state.toDos.find((toDo: { id: number; }) => toDo.id === action.payload);
      return {
        ...state,
        toDos: state.toDos.filter((toDo: { id: number; }) => toDo.id !== action.payload),
        completed: [...state.completed, { ...target }],
      };
    case UNCOMPLETE:
      const aTarget = state.completed.find(
        (toDo: { id: number; }) => toDo.id === action.payload
      );
      return {
        ...state,
        toDos: [...state.toDos, { ...aTarget }],
        completed: state.completed.filter(
          (complete: { id: number; }) => complete.id !== action.payload
        ),
      };
    case EDIT:
      const bTarget = state.toDos.find((toDo: { id: number; }) => toDo.id === action.id);
      const rest = state.toDos.filter((toDo: { id: number; }) => toDo.id !== action.id);
      return {
        ...state,
        toDos: rest.concat({ ...bTarget, text: action.payload }),
      };
    default:
      return;
  }
};

export default Reducer;

此代码为'context.tsx'代码。

import React, { createContext, useReducer, useContext } from 'react';
import Reducer, { initialState } from "./reducer";

export type Todo = {
  id: number;
  text: string;
  done: boolean;
};

export type TodosState = Todo[];

const ToDosContext = createContext<Array<Todo> | any>(null);

const ToDosProvider = ({ children }: { children: React.ReactNode }) => {
  const [state, dispatch] = useReducer(Reducer, initialState);
  return (
    <ToDosContext.Provider value={{ state, dispatch }}>
      {children}
    </ToDosContext.Provider>
  );
};

export const useTodosDispatch = () => {
  const { dispatch } = useContext(ToDosContext);
  return dispatch;
};

export const useTodosState = () => {
  const { state } = useContext(ToDosContext);
  return state;
};

export default ToDosProvider;

键入状态

interface IReducer {
  state: any;
  action: any;
}

这种类型不是特别有用,因为您的 state 可以是任何类型!

这导致您不得不在代码中进一步做出断言,例如在调用 state.toDos.filter() 时必须添加 { id: number; },如果您的 state 则没有必要已正确输入。

它还会导致您忽略错误,例如 default 中的 return; 而不是 return state;。打字稿编译器应该会处理这些事情,但在这种情况下它不会显示错误,因为 undefined 仍然可以分配给您的 any 状态类型。

看起来您的状态是一个具有属性 toDoscompleted 的对象,其中两个属性都是 Todo 对象的 array。似乎您实际上并没有在 Todo 类型上使用 done 属性,而是使用单独的数组来查看哪些已完成或未完成。我不确定你是否想要 done 属性 在我们 select 来自州的待办事项时添加,或者它是否只是旧代码的遗留物并且不需要。

interface Todo {
  id: string;
  text: string;
}

interface State {
  toDos: Todo[];
  completed: Todo[];
}

键入操作

就您的操作而言,您可以通过将操作类型定义为特定操作的所有类型的联合来获得最大的类型安全性。这就是开始感觉 “这太让人头疼了,只需使用 Redux Toolkit,因为该工具包确实消除了很多样板文件。

对于大多数操作,待办事项的数字 id 似乎就是您的 action.payload。但是对于您的编辑操作,id 是 action.id 并且文本是有效负载。我不喜欢这种不一致的情况,但我只想输入你在这里的内容而不是更改 reducer。

type Action = {
  type: typeof ADD | typeof DEL | typeof COMPLETE | typeof UNCOMPLETE;
  payload: string;
} | {
  type: typeof EDIT;
  payload: string;
  id: string;
}

打字减速器

当我开始为 reducer 添加类型时,一个我以前没有注意到的重大错误被突出显示了!这就是正确打字如此重要的原因。您的 switch 语句根据 action 进行切换,而它应该在 action.type.

现在您的 reducer 接受一个参数,它是一个具有属性 stateaction 的对象。但这不是 reducer,如果你不接受正确的参数,它就不能与 useReducer(或 redux)一起工作。 reducer 函数看起来像 (state, action) => newState.

const reducer = (state: State, action: Action): State

修复此问题后,我开始看到更多错误被突出显示。事实证明,您通过调用 uuidv4() 创建的 idstring 而不是 number。所以在任何地方,你输入的待办事项 id 作为 number 都是错误的。但是在回调中有 (toDo: { id: number; }) 的任何地方,您都可以更改为 toDo,因为类型从数组中已知。

在将 target 添加到数组时,由于可能找不到匹配项并且 targetundefined,因此在完整、不完整和编辑情况下存在错误.我们可以使这个有条件。

  completed: target ? [...state.completed, { ...target }] : state.completed,

我们必须在这么多地方做同样的事情,这不是很好。遇到这种情况,您可能会开始考虑用于更改数组 toDos 的辅助实用函数。或者,使用 Redux Toolkit.

一切都变得更容易

键入上下文

在您的原始类型中,您说的是上下文值是一个包含 属性 done 的 toDos 数组。我不确定您是否打算从状态映射到单个数组,或者这是一个错误。我假设这是一个错误。

但是如果你想要那种格式,它是:

const withDone = (state: State): Array<Todo & {done: boolean}> => {
  return [
    ...state.toDos.map(todo => ({...todo, done: false})),
    ...state.completed.map(todo => ({...todo, done: true})),
  ]
}

我们不需要在 useReducer 上指定任何类型,因为这些都可以从我们的强类型 reducer 函数中推断出来。哇!但是我们确实需要指定上下文值的类型。

interface ContextValue {
  state: State;
  dispatch: React.Dispatch<Action>;
}

const ToDosContext = createContext<ContextValue>(null);

除非您在 tsconfig 中关闭 strictNullChecks,否则您可能会在将 null 分配为上下文的初始值时出错,因为 null 不是可分配给 ContextValue。所以我们必须给它一个初始值,这是如果在没有 Provider 的情况下访问上下文时将使用的值,以提供实际值。

const ToDosContext = createContext<ContextValue>({
  state: initialState,
  dispatch: () => { console.error("called dispatch outside of a ToDosContext Provider")}
});

输入上下文后,useTodosDispatchuseTodosState 会自动推断出正确的 return 类型。虽然我更喜欢直截了当。

export const useTodosDispatch = (): React.Dispatch<Action> => { ... };

export const useTodosState = (): State => { ... };

现在大家在一起

我们终于没有错误了!当我向事物添加类型时,我发现了很多以前被所有 any 隐藏的错误。这是完整的代码:

import React, { createContext, useContext, useReducer } from "react";
import { v4 as uuidv4 } from "uuid";
import { ADD, DEL, COMPLETE, UNCOMPLETE, EDIT } from "./actions";

interface Todo {
  id: string;
  text: string;
}

interface State {
  toDos: Todo[];
  completed: Todo[];
}

type Action = {
  type: typeof ADD | typeof DEL | typeof COMPLETE | typeof UNCOMPLETE;
  payload: string;
} | {
  type: typeof EDIT;
  payload: string;
  id: string;
}

const reducer = (state: State, action: Action): State => {
  switch (action.type) {
    case ADD:
      return {
        ...state,
        toDos: [...state.toDos, { text: action.payload, id: uuidv4() }],
      };
    case DEL:
      return {
        ...state,
        toDos: state.toDos.filter((toDo) => toDo.id !== action.payload),
      };
    case COMPLETE:
      const target = state.toDos.find((toDo) => toDo.id === action.payload);
      return {
        ...state,
        toDos: state.toDos.filter((toDo) => toDo.id !== action.payload),
        completed: target ? [...state.completed, { ...target }] : state.completed,
      };
    case UNCOMPLETE:
      const aTarget = state.completed.find(
        (toDo) => toDo.id === action.payload
      );
      return {
        ...state,
        toDos: aTarget ? [...state.toDos, { ...aTarget }] : state.toDos,
        completed: state.completed.filter(
          (complete) => complete.id !== action.payload
        ),
      };
    case EDIT:
      const bTarget = state.toDos.find((toDo) => toDo.id === action.id);
      const rest = state.toDos.filter((toDo) => toDo.id !== action.id);
      return {
        ...state,
        toDos: bTarget ? rest.concat({ ...bTarget, text: action.payload }) : rest,
      };
    default:
      return state;
  }
};

interface ContextValue {
  state: State;
  dispatch: React.Dispatch<Action>;
}

export const initialState = {
  toDos: [],
  completed: [],
};

const ToDosContext = createContext<ContextValue>({
  state: initialState,
  dispatch: () => { console.error("called dispatch outside of a ToDosContext Provider") }
});


const ToDosProvider = ({ children }: { children: React.ReactNode }) => {
  const [state, dispatch] = useReducer(reducer, initialState);
  return (
    <ToDosContext.Provider value={{ state, dispatch }}>
      {children}
    </ToDosContext.Provider>
  );
};

export const useTodosDispatch = (): React.Dispatch<Action> => {
  const { dispatch } = useContext(ToDosContext);
  return dispatch;
};

export const useTodosState = (): State => {
  const { state } = useContext(ToDosContext);
  return state;
};

Typescript Playground Link