首页 > 解决方案 > TypeError:无法解构“对象(...)(...)”的属性“toDos”,因为它未定义

问题描述

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

在我看来,“减速器”工作不正常。我该如何操作? 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;

标签: typescriptreact-hooksreducersreact-tsx

解决方案


打字状态

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

这种类型并不是特别有用,因为你state可以是任何东西!

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

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

看起来您的状态是一个具有属性的对象,toDos并且completed两个属性都是对象的arrays Todo。似乎您实际上并没有使用done该类型的属性Todo,而是使用单独的数组来查看哪些已完成。我不确定您是否希望done在我们从状态中选择 toDos 时添加该属性,或者它是否只是旧代码的遗物并且不需要。

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

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

打字动作

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

对于大多数操作,看起来idtoDo 的数字是您的action.payload. 但是对于您的编辑操作,id 是action.id,文本是有效负载。我不喜欢这种不一致,但我只是在此处输入您所拥有的内容,而不是更改减速器。

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

打字减速机

当我开始为减速器添加类型时,一个我以前没有注意到的重大错误被突出显示!这就是为什么正确的类型如此重要的原因。您的switch语句正在切换基于action它应该何时打开action.type

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

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

当我解决这个问题时,我开始看到更多错误被突出显示。事实证明,id您通过调用创建的uuidv4()是 astring而不是 a number。因此,无论您在哪里输入待办事项 idnumber都是错误的。但是(toDo: { id: number; })在回调中的任何地方,您都可以更改为,toDo因为类型是从数组中已知的。

target将 a 添加到数组时,在完成、未完成和编辑案例中存在错误,因为可能找不到匹配项并且targetis undefined。我们可以把它作为条件。

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

我们必须在这么多地方做同样的事情,这并不好。这类事情是您可能开始考虑用于更改数组 toDos 的辅助实用程序函数的地方。或者,使用Redux Toolkit一切都变得更简单。

键入上下文

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

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

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);

除非您strictNullChecks在. tsconfig_ null_ 所以我们必须给它一个初始值,这是在没有 a 的情况下访问上下文时将使用的值,以给出一个真正的值。nullContextValueProvider

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

随着我们的上下文类型化,useTodosDispatchuseTodosState自动推断出正确的返回类型。虽然我更喜欢明确。

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;
};

打字稿游乐场链接


推荐阅读