如何基于闭包类型收窄TypeScript泛型函数的类型?
问题:React Hook返回的断言函数无法收窄外部状态的类型
我写了一个React Hook,它接收枚举值作为状态,返回一个函数用来判断状态是否刚切换到目标值。但调用这个返回函数时,条件分支里的原状态变量无法被正确收窄类型——比如下面示例中,status始终是SomeStatusEnum,不会被收窄为SUCCESS:
// status: SomeStatusEnum const didTransitionTo = useDidTransition(status); if (didTransitionTo(SomeStatusEnum.SUCCESS)) { // 此处status的类型仍然是SomeStatusEnum,不是SUCCESS }
Hook的原始实现:
/** * 传入状态值T,返回断言函数,判断是否刚进入匹配的状态 */ export function useDidTransition<S>(status: S) { const prevStatus = usePreviousValue(status); /** * 判断状态是否从任意其他值切换到目标值to */ function didTransitionTo(to: S): boolean { return to !== prevStatus && to === status; } return didTransitionTo; }
我尝试用类型守卫修复,但遇到错误Cannot find parameter 'status'——因为status属于Hook的外部作用域,不是didTransitionTo的参数,没法直接在类型守卫里断言它:
export function useDidTransition<S>(status: S) { const prevStatus = usePreviousValue(status); // 报错:找不到参数status function didTransitionTo<To extends S>(to: To): status is To { return to !== prevStatus && to === status; } return didTransitionTo; }
注:我宁愿弃用这个Hook,也不愿意让didTransitionTo额外接收status参数。
解决方案
核心原因
TypeScript的类型守卫只能直接收窄当前函数参数的类型,无法直接作用于外部作用域的变量。之前的写法试图在返回函数里断言外部的status,这不符合TypeScript的类型检查规则。
可行实现方案
方案1:返回包含类型守卫方法的对象
我们可以给返回的函数附加一个is方法,专门用来做类型守卫——这个方法定义在Hook内部,能捕获外部的status变量,从而让TypeScript识别到类型断言的目标:
import { useRef } from 'react'; // 自定义的usePreviousValue实现(假设你已经有这个Hook) function usePreviousValue<T>(value: T) { const ref = useRef<T>(); const prevValue = ref.current; ref.current = value; return prevValue; } export function useDidTransition<S>(status: S) { const prevStatus = usePreviousValue(status); // 给返回的函数附加is方法 const transitionChecker = (to: S): boolean => to !== prevStatus && to === status; transitionChecker.is = <To extends S>(to: To): status is To => { return to !== prevStatus && to === status; }; return transitionChecker; }
使用方式:
enum SomeStatusEnum { IDLE = 'IDLE', LOADING = 'LOADING', SUCCESS = 'SUCCESS', } function MyComponent() { const [status, setStatus] = React.useState<SomeStatusEnum>(SomeStatusEnum.IDLE); const didTransitionTo = useDidTransition(status); if (didTransitionTo.is(SomeStatusEnum.SUCCESS)) { // 此处status被正确收窄为SomeStatusEnum.SUCCESS console.log(status); // 类型为SUCCESS } return <div>...</div>; }
方案2:使用断言函数(asserts)
如果可以接受断言失败时抛出错误,我们可以使用TypeScript的asserts语法,直接让返回函数成为一个断言工具:
export function useDidTransition<S>(status: S) { const prevStatus = usePreviousValue(status); return <To extends S>(to: To): asserts status is To => { const didTransition = to !== prevStatus && to === status; if (!didTransition) { throw new Error(`状态未切换到目标值:${String(to)}`); } }; }
使用方式:
const didTransitionTo = useDidTransition(status); // 条件判断中使用,断言成功则收窄类型 if (didTransitionTo(SomeStatusEnum.SUCCESS)) { // status类型为SUCCESS } // 或者直接调用,失败则抛出错误,之后的代码中status类型自动收窄 didTransitionTo(SomeStatusEnum.SUCCESS); console.log(status); // 类型为SUCCESS
方案说明
- 方案1兼容原有的布尔值返回逻辑,同时通过附加的
is方法实现类型收窄,不会改变原有调用习惯太多。 - 方案2更直接,但属于“断言”而非“判断”,失败会抛出异常,适合必须确保状态切换完成的场景。
内容的提问来源于stack exchange,提问作者Ross Mackay
相关产品推荐
相关产品推荐

