最近向 @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
还木有评论哦,快来抢沙发吧~