You need to enable JavaScript to run this app.
优惠活动
大模型
产品
解决方案
定价
更多

如何基于闭包类型收窄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

相关产品推荐
方舟 Agent Plan

超全模态模型 × Harness 升级,最新支持 Deepseek-V4.1-Flash、GLM-5.3 系列、Doubao-Seedream-5.0-pro、Kimi-K3 (部分), 限时 9.9 元起

最近更新时间:2026.07.08 00:23:26