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

如何用typeof收窄TypeScript泛型?自定义URL状态Hook报错

解决useSearchParamsState中TypeScript类型收窄失败的问题

问题核心:你想通过typeof defaultValue把泛型参数TState收窄为string,从而复用stringValueConverters,但TypeScript无法将运行时的类型判断映射到编译时的泛型约束,导致类型不匹配报错。

为什么会报错?

泛型参数TState在编译阶段是一个抽象类型,而typeof defaultValue是运行时的类型检查。TypeScript无法从运行时的判断反推编译时的泛型类型,所以即便你判断了defaultValue是string,它也不会自动把TState收窄为string,自然就会出现ValueConverters<string>不能赋值给ValueConverters<TState>的错误。

解决方案一:函数重载(最清晰)

通过重载函数签名,明确区分string类型和自定义类型的场景,让TypeScript在编译时就能匹配到正确的类型:

import { useState, useEffect } from 'react';

type ValueConverters<T> = {
  parse: (str: string) => T;
  stringify: (value: T) => string;
};

// 默认的string类型转换器
const stringValueConverters: ValueConverters<string> = {
  parse: (str) => str,
  stringify: (value) => value,
};

// 重载1:默认string类型,转换器可选
function useSearchParamsState(
  key: string,
  defaultValue: string,
  customValueConverters?: ValueConverters<string>
): [string, React.Dispatch<React.SetStateAction<string>>];

// 重载2:自定义类型,必须传入转换器
function useSearchParamsState<TState>(
  key: string,
  defaultValue: TState,
  customValueConverters: ValueConverters<TState>
): [TState, React.Dispatch<React.SetStateAction<TState>>];

// Hook实现
function useSearchParamsState<TState>(
  key: string,
  defaultValue: TState,
  customValueConverters?: ValueConverters<TState>
) {
  // 利用重载保证:当TState不是string时,customValueConverters必传,不会走到默认转换器分支
  const converters = customValueConverters ?? (stringValueConverters as ValueConverters<TState>);

  // 从URL读取初始值
  const [state, setState] = useState<TState>(() => {
    const searchParams = new URLSearchParams(window.location.search);
    const valueStr = searchParams.get(key);
    return valueStr ? converters.parse(valueStr) : defaultValue;
  });

  // 同步状态到URL
  useEffect(() => {
    const searchParams = new URLSearchParams(window.location.search);
    const valueStr = converters.stringify(state);
    searchParams.set(key, valueStr);
    window.history.replaceState(null, '', `?${searchParams.toString()}`);
  }, [state, key, converters]);

  return [state, setState];
}

这样调用时,TypeScript会自动识别场景:

  • 传入string类型的defaultValue,转换器可选
  • 传入其他类型的defaultValue,必须传对应的转换器

解决方案二:条件类型+类型断言

如果不想用重载,可以通过条件类型约束泛型,配合类型断言解决:

import { useState, useEffect } from 'react';

type ValueConverters<T> = {
  parse: (str: string) => T;
  stringify: (value: T) => string;
};

const stringValueConverters: ValueConverters<string> = {
  parse: (str) => str,
  stringify: (value) => value,
};

// 定义默认转换器的条件类型:仅当TState是string时才生效
type DefaultConverter<T> = T extends string ? ValueConverters<string> : never;

function useSearchParamsState<TState = string>(
  key: string,
  defaultValue: TState,
  customValueConverters?: ValueConverters<TState> | DefaultConverter<TState>
) {
  const converters = customValueConverters ?? (stringValueConverters as ValueConverters<TState>);

  // 后续逻辑同上面的实现...
  const [state, setState] = useState<TState>(() => {
    const searchParams = new URLSearchParams(window.location.search);
    const valueStr = searchParams.get(key);
    return valueStr ? converters.parse(valueStr) : defaultValue;
  });

  useEffect(() => {
    const searchParams = new URLSearchParams(window.location.search);
    searchParams.set(key, converters.stringify(state));
    window.history.replaceState(null, '', `?${searchParams.toString()}`);
  }, [state, key, converters]);

  return [state, setState];
}

这里的类型断言是安全的,因为条件类型DefaultConverter<T>已经约束了当TState不是string时,customValueConverters必须传入,不会使用默认的string转换器。


内容的提问来源于stack exchange,提问作者John Reilly

相关产品推荐
方舟 Agent Plan

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

最近更新时间:2026.08.22 11:57:15