useReducer & Typescript

reacttypescript

useReducer与Typescript的类型计算。

例子

从一段简单的代码开始。

import React, { useReducer } from "react";

type State = {
  count: number;
};

type Action = {
  type: "increase" | "decrease";
};

export default function Demo() {

  const initialState: State = {
    count: 0
  };

  const reducer = (prev: State, action: Action) => {
    switch (action.type) {
      case "increase":
        return { count: prev.count + 1 };
    }
  };

  const [state, dispatch] = useReducer(reducer, initialState);

  return (
    <>
      <button onClick={() => dispatch({ type: "increase" })}>+</button>
      <button onClick={() => dispatch({ type: "decrease" })}>-</button>
      <span>{state.count}</span>
    </>
  );
}

如果简单复制粘贴代码进vscode,可以看到在useReducer initialState 位置显示如下错误。

Argument of type 'State' is not assignable to parameter of type 'never'.ts(2345)
const initialState: State

但是为什么useReducer的第二个参数类型会是never呢?

检查reactindex.d.ts可以看到如下定义

function useReducer<R extends Reducer<any, any>, I(
       reducer: R,
       initializerArg: I & ReducerState<R>,
       initializer: (arg: I & ReducerState<R>) =>ReducerState<R>
   ): [ReducerState<R>, Dispatch<ReducerAction<R>>;

type ReducerState<R extends Reducer<any, any>> = R extends Reducer<infer S, any> ? S : never;

type Reducer<S, A> = (prevState: S, action: A) => S;

显而易见此时的reducer 缺少了对{type:"decrease"}的处理。如果hover到reducer上,可以看到这个函数实际的类型:

const reducer: (prev: State, action: Action) => {
    count: number;
} | undefined

从中可以得出,例子中useReducer传入的reducer传回的类型从根本上不满足Reducer类型的定义(传回的类型不满足State类型定义),因而导致ReducerState<R>的值为never

进而,initializerArg: I & ReducerState<R>的类型为never,所以出现了此时的报错。

改进

其实说不定聪明的你已经注意到了,应该在reducer定义的位置显示表示传回参数类型,这样在不满足时能够显示正确的错误。

代码如下

const reducer = (prev: State, action: Action): State => {
//...not always returning State type
};

可以看到正确的报错

Function lacks ending return statement and return type does not include 'undefined'.

完善reducer

const reducer = (prev: State, action: Action): State => {
  switch (action.type) {
    case "increase":
      return { count: prev.count + 1 };
    case "decrease":
      return { count: prev.count - 1 };
  }
};

就能正常编译运行了。

之前阅览的某些教程为了取消报错,修改prev: Stateprev: any,同样不显示标记返回类型,个人认为不是好的做法。

参考