如何让React useTypedQueryParams钩子通过单一类型参数推断解码器返回值?
React钩子useTypedQueryParams的类型约束优化问题
我开发了一个React钩子useTypedQueryParams,可以把对象内容写入URL查询参数,同时为每个键接收一个decoder参数,用来将字符串类型的参数解析/转换回原始类型。这个钩子接受一个单一类型参数来描述对象结构,整体类型检查已经完善,但存在一处不足:无法从该类型参数推断出decoder的返回值。想请教是否可以让这个钩子使用同一个类型参数,同时约束输入对象和解码器的返回值?
当前实现已满足以下类型检查要求:
- 若
T中的键没有对应的decoder,TypeScript会报错; - 若
decoder没有T中对应的键,TypeScript会报错; - 若
setQueryParams的参数与T不匹配,TypeScript会报错。
但解码器的返回值未被校验,所有queryParam的值均基于T进行类型定义。
当前代码实现
export function useTypedQueryParams<T extends Record<string, unknown>>( paramDecoders: Record<keyof T, (value?: string) => T[keyof T]>, ): [T, (params: T) => void] { const { search } = useLocation(); const history = useHistory(); const paramDecodersRef = useRef(paramDecoders); const baseQueryParams = useMemo( () => Object.entries(queryString.parse(search)).reduce( (acc, [key, value]) => !paramDecodersRef.current[key as keyof T] ? { ...acc, [key]: value, } : acc, {}, ), [search], ); const parsed: T = useMemo(() => { const parsedQuery: Record<string, string> = queryString.parse(search); return Object.keys(paramDecodersRef.current).reduce( (parsedParams, paramKey) => { const decoder = paramDecodersRef.current[paramKey]; const value = parsedQuery[paramKey as string]; return { ...parsedParams, [paramKey]: decoder ? decoder(value) : value, }; }, {} as T, ); }, [search]); const setQueryParams = useCallback( (updatedParams: T) => { const newQueryString = queryString.stringify({ ...baseQueryParams, ...updatedParams, }); history.push({ search: `?${newQueryString}`, }); }, [history, baseQueryParams], ); return [parsed, setQueryParams]; }
使用示例(存在未被捕获的类型错误)
const [queryParams, setQueryParams] = useTypedQueryParams<{ foo: string | null; bar: number | null; }>({ foo: (value?: string) => value || null, // TODO: TypeScript无法捕获这里bar返回字符串而非数字的错误 bar: (value?: string) => value || null, });
解决方案:精确约束解码器返回值
要解决这个问题,只需修改paramDecoders的类型定义,让每个键对应的解码器返回值严格匹配T中对应键的类型,而不是T所有值类型的联合。
修改后的钩子代码
import { useLocation, useHistory } from 'react-router-dom'; import queryString from 'query-string'; import { useMemo, useCallback, useRef } from 'react'; export function useTypedQueryParams<T extends Record<string, unknown>>( // 关键修改:为每个键K单独约束解码器返回值为T[K] paramDecoders: { [K in keyof T]: (value?: string) => T[K] }, // 可选优化:允许只更新部分参数,使用Partial<T> ): [T, (params: Partial<T>) => void] { const { search } = useLocation(); const history = useHistory(); const paramDecodersRef = useRef(paramDecoders); const baseQueryParams = useMemo( () => Object.entries(queryString.parse(search)).reduce( (acc, [key, value]) => !paramDecodersRef.current[key as keyof T] ? { ...acc, [key]: value, } : acc, {}, ), [search], ); const parsed: T = useMemo(() => { const parsedQuery = queryString.parse(search) as Record<string, string | undefined>; return Object.keys(paramDecodersRef.current).reduce((parsedParams, paramKey) => { const decoder = paramDecodersRef.current[paramKey as keyof T]; const value = parsedQuery[paramKey]; return { ...parsedParams, [paramKey]: decoder(value), }; }, {} as T); }, [search]); const setQueryParams = useCallback( (updatedParams: Partial<T>) => { const newQueryString = queryString.stringify({ ...baseQueryParams, ...updatedParams, }); history.push({ search: newQueryString ? `?${newQueryString}` : '', }); }, [history, baseQueryParams], ); return [parsed, setQueryParams]; }
修改后的使用示例(TypeScript会捕获错误)
const [queryParams, setQueryParams] = useTypedQueryParams<{ foo: string | null; bar: number | null; }>({ foo: (value?: string) => value || null, // 此时TypeScript会报错:类型'string | null'不能赋值给类型'number | null' bar: (value?: string) => value || null, });
修改说明
- 核心类型约束调整:将
paramDecoders的类型从Record<keyof T, (value?: string) => T[keyof T]>改为{ [K in keyof T]: (value?: string) => T[K] },实现了对每个解码器返回值的精确约束。 - 可选优化:将
setQueryParams的参数改为Partial<T>,允许只更新部分查询参数,更贴合实际使用场景。 - 类型严谨性修正:修正了
parsedQuery的类型定义,避免不必要的类型断言。
内容的提问来源于stack exchange,提问作者David Sharff
相关产品推荐
相关产品推荐

