如何用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
相关产品推荐
相关产品推荐

