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

如何正确将searchParams迭代器归约为精确类型T的查询参数对象

实现返回严格类型查询参数的自定义React Hook

需求

编写自定义Hook获取URL查询参数,返回值严格匹配泛型T的类型定义。

原有问题实现

export function useQueryParams<T>(): [
  T,
  ({ key, value }: { key: keyof T; value: string }) => void,
] {
  const [searchParams, setSearchParams] = useSearchParams()
  const entries = Array.from(searchParams.entries()).reduce(
    (acc, [key, value]) => {
      return { ...acc, [key]: value }
    },
    {} as T,
  )

  const setQueryParam = React.useCallback(
    ({ key, value }) => {
      searchParams.set(key, value)
      setSearchParams(searchParams)
    },
    [searchParams, setSearchParams],
  )

  return [entries, setQueryParam]
}

存在的问题

  • URL包含T定义外的额外查询参数时不会抛出错误
  • reduce实际返回Partial<T>类型,通过{} as T强制类型断言绕过类型检查,编译时和运行时类型都无法得到保障
  • setQueryParam直接修改原searchParams实例后传入更新方法,可能因引用不变导致组件不触发重渲染

正确实现方案

TypeScript类型仅在编译阶段生效,运行时会完全擦除泛型T的类型信息,因此必须传入运行时可访问的合法键列表作为校验依据,同时做双重校验保证返回值完全匹配T的结构:

  1. 遍历URL查询参数时,拦截所有不在合法键列表内的额外参数,直接抛出错误
  2. 参数收集完成后,校验所有T定义的键都存在,缺失必填参数直接抛出错误
  3. 支持传入自定义值解析函数,适配数字、布尔值等非字符串类型参数
  4. 修复参数更新方法的引用问题

最终代码

import { useSearchParams } from 'react-router-dom';
import React from 'react';

// 定义参数值解析函数类型,支持将字符串参数转换为T中定义的任意类型
type ParseMap<T> = {
  [K in keyof T]: (value: string) => T[K];
};

/**
 * 获取严格类型的URL查询参数
 * @param allowedKeys T类型对应的所有合法查询参数键列表(运行时校验用)
 * @param parsers 可选,各参数对应的自定义解析函数,默认直接返回字符串值
 */
export function useQueryParams<T extends Record<string, unknown>>(
  allowedKeys: Array<keyof T>,
  parsers?: Partial<ParseMap<T>>
): [T, ({ key, value }: { key: keyof T; value: string }) => void] {
  const [searchParams, setSearchParams] = useSearchParams();

  const entries = React.useMemo(() => {
    const result = {} as T;
    // 第一步:遍历所有URL参数,拦截非法额外字段
    for (const [key, rawValue] of searchParams.entries()) {
      if (!allowedKeys.includes(key as keyof T)) {
        throw new Error(`非法查询参数:${key},该字段不在参数类型定义范围内`);
      }
      // 优先使用自定义解析器转换值,无解析器则直接返回字符串
      const parser = parsers?.[key as keyof T] ?? ((val: string) => val as T[keyof T]);
      result[key as keyof T] = parser(rawValue);
    }
    // 第二步:校验所有必填参数是否存在
    for (const requiredKey of allowedKeys) {
      if (!(requiredKey in result)) {
        throw new Error(`缺失必填查询参数:${String(requiredKey)}`);
      }
    }
    return result;
  }, [searchParams, allowedKeys, parsers]);

  const setQueryParam = React.useCallback(
    ({ key, value }: { key: keyof T; value: string }) => {
      // 创建新的URLSearchParams实例,避免修改原对象引用导致的更新失效
      const newSearchParams = new URLSearchParams(searchParams);
      newSearchParams.set(key as string, value);
      setSearchParams(newSearchParams);
    },
    [searchParams, setSearchParams]
  );

  return [entries, setQueryParam];
}

使用示例

// 定义查询参数的严格类型
type OrderListQuery = {
  orderId: string;
  page: number;
  isPaid: boolean;
};

// 传入合法键列表和对应的值解析函数
const [queryParams, setQueryParam] = useQueryParams<OrderListQuery>(
  ['orderId', 'page', 'isPaid'],
  {
    page: (val) => Number(val),
    isPaid: (val) => val === 'true'
  }
);

// queryParams 类型严格为 OrderListQuery,多余/缺失参数都会在运行时抛出错误
// setQueryParam 仅接受key为OrderListQuery的键,编译时就会拦截非法key传值

关键说明

  • 不要尝试通过类型体操跳过allowedKeys的传入:TS类型编译后会被擦除,没有运行时的键列表就根本无法做参数合法性校验,所有声称不需要传合法键的实现本质上都是靠类型断言欺骗编译器,没有实际的类型安全保障。
  • 如果需要支持可选参数,可以将校验逻辑调整为仅校验T中必填的键,对可选键不做存在性检查即可。

内容的提问来源于stack exchange,提问作者Antonio Santoro

相关产品推荐
方舟 Agent Plan

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

最近更新时间:2026.08.30 22:48:19