记 react 项目在 TypeScript 化中的一个坑,以及相应的类型改动

前端这点事 61 0

最近向 @types/react 提交了一个变动,改动了 useReducer 的定义,相信各位读者如果要 TypeScript 化,或者已经 TS 化的话,有可能会收到影响。

通过安装 @types/react@16.9.17 可以使用新的类型定义,这里简单的介绍一下这是怎样的一个能力。

我们首先看一下 react 官方 useReducer 文档中的 case,参见

const initialState = {count: 0};

function reducer(state, action) {
  switch (action.type) {
    case 'increment':
      return {count: state.count + 1};
    case 'decrement':
      return {count: state.count - 1};
    default:
      throw new Error();
  }
}

function Counter() {
  const [state, dispatch] = useReducer(reducer, initialState);
  return (
    <>
      Count: {state.count}
      <button onClick={() => dispatch({type: 'decrement'})}>-</button>
      <button onClick={() => dispatch({type: 'increment'})}>+</button>
    </>
  );
}

@types/react 对这种 case 的支持是很充分的。然而,我们可能会写一个不需要 action 的 reducer,这种情况下,其实可以认为这个 reducer 只接受唯一可能的 action,如:

const [count, increase] = useReducer(v => v + 1, 0);

有些人会说,为什么不用 useState 来开发这种需求,如果用 useState:

const [count, setCount] = useState(0);
const increase = setCount(v => v + 1);

然而,increase 会在每次 render 的时候进行初始化,意味着每一次它是不一样的,如果不希望对子组件产生影响,就需要写成:

const [count, setCount] = useState(0);
const increase = useCallback(
  () => setCount(v => v + 1),
  []
);

这样的代码我是不太能忍受的,react 在内部用类似 reducer = (state, action) => action 的方式实现 setState,结果开发者又要用 setState 重新实现 reducer。。。体会一下 code smell。。。

然而,@types/react 并没有考虑这个情况,这样的写法会得到一个 TS 报错:

const [count, increase] = useReducer(v => v + 1, 0);

// TS2554: Expected 1 arguments, but got 0. increase();

在考虑 reducer 永远接受 action 的情况下,这是正确的:

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

type Dispatch<A> = (action: A) => void;

// 伪代码,这里有 R 满足 R extends Reducer<S, A> type UseReducer<R, S, A> = (reducer: R, initialArg: S) => [S, Dispatch<A>];

Dispatch<A> 会检查 dispatch(action) 中 action 的类型永远为 Reducer<S, A> 中的 A。

这里涉及到 TS 里的 类型推导功能,当我写:

function identity<T>(arg: T): T {
    return arg;
}

const result = identity('myString');

TS 会识别 'myString' 的 类型为 string,并且推断出 result 的类型也为 string。

所以,回到上面,当我们 dispatch 传入空值的时候,TS 会报错并期望一个符合 action: A 的值。

然而,我们不能简单的通过 (action?: A) => void 来解决这个问题,当 Reducer 接受 action 参数的时候,我们希望 dispatch 一个确实符合的 action。而当 Reducer 不接受 action 参数的时候,我们才希望 dispatch 可以不传入一个 action,即:

const [, dispatch] = useReducer((sum, n) => sum + n);

// Pass dispatch(1);

// ExpectError dispatch();

const [, dispatchWithoutAction] = useReducer(sum => sum + 1);

// Pass dispatchWithoutAction();

我们要使用 TS 的另一个特性 overload(重载)来实现这个需求,可以简化为:

type FuncBlank = () => void;
type Func<T> = (value: T) => void;

function createFunc(): FuncBlank;
function createFunc<T>(value: T): Func<T>;
function createFunc<T>(value?: T): FuncBlank | Func<T> {
    // return ... }

这样,调用 createFunc() 会得到一个 FuncBlank,调用 createFunc('myString') 会得到一个 Func<string>

在 useReducer 的重载上也是一样,我们需要判断 reducer 是一个带 action 的 reducer,或者是一个不带 action 的 reducer,来决定我们返回的 dispatch 的类型是否接受参数。

标签: React

发表评论 (已有0条评论)

还木有评论哦,快来抢沙发吧~