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

如何让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,
});

修改说明

  1. 核心类型约束调整:将paramDecoders的类型从Record<keyof T, (value?: string) => T[keyof T]>改为{ [K in keyof T]: (value?: string) => T[K] },实现了对每个解码器返回值的精确约束。
  2. 可选优化:将setQueryParams的参数改为Partial<T>,允许只更新部分查询参数,更贴合实际使用场景。
  3. 类型严谨性修正:修正了parsedQuery的类型定义,避免不必要的类型断言。

内容的提问来源于stack exchange,提问作者David Sharff

相关产品推荐
方舟 Agent Plan

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

最近更新时间:2026.07.29 09:35:27