当前位置: 首页 > 知识库问答 >
问题:

javascript - TS,函数有两个参数,根据第一个参数约束第二个参数,并且推断出最终的结果?

衡子安
2024-07-26

TS,函数有两个参数,根据第一个参数约束第二个参数,并且推断出最终的结果。

比如我需要一个合并path和参数的函数,根据path来约束所传的参数,最终拼接path和params得出最终的string,比如:

type Path2Params = {
  '/order/detail': { orderId: string };
  '/product/list': { type: string; pageSize: string; pageNo: string };
};
const orderParams: Path2Params['/order/detail'] = { orderId: '123' };
const productListParams: Path2Params['/product/list'] = { type: 'electronics', pageSize: '10', pageNo: '1' };

通过函数能推断出orderUrl为/order/detail?orderId=123,productListUrl为/product/list?type=electronics&pageSize=10&pageNo=1

这是我自己的实现,但是有一些问题:

type Path2Params = {
  '/order/detail': { orderId: string };
  '/product/list': { type: string; pageSize: string; pageNo: string };
};

type BuildQueryString<TParams extends Record<string, string>> = {
  [K in keyof TParams]: `${Extract<K, string>}=${TParams[K]}`;
}[keyof TParams];

type FullUrl<
  TPath extends string,
  TParams extends Record<string, string>,
> = `${TPath}${TParams extends Record<string, never> ? '' : '?'}${BuildQueryString<TParams>}`;

/**
 * 构建一个带有查询参数的URL字符串。
 * @param path 路径参数,必须是`Path2Params`类型的键。
 * @param params 查询参数,根据路径参数`path`在`Path2Params`中定义的类型。
 * @returns 返回一个完整的URL字符串,包括路径和编码后的查询参数。
 * @template TPath `Path2Params`中的键类型,用于确保类型安全。
 * @template TParams `Path2Params`中由`TPath`指定的键对应的值的类型,用于确保类型安全。
 */
function buildStringWithParams<TPath extends keyof Path2Params, TParams extends Path2Params[TPath]>(
  path: TPath,
  params: TParams,
): FullUrl<TPath, TParams> {
  // 优化:使用数组和join来减少字符串操作的开销
  const encodedParams = Object.entries(params).map(([key, value]) => {
    // 安全性优化:确保value经过适当的编码
    const encodedValue = encodeURIComponent(value);
    // 类型安全优化:不再需要as string,因为value已经被编码为字符串
    return `${encodeURIComponent(key)}=${encodedValue}`;
  });
  const queryString = encodedParams.join('&');
  return `${path}${queryString ? '?' : ''}${queryString}` as FullUrl<TPath, TParams>;
}

const orderParams: Path2Params['/order/detail'] = { orderId: '123' };
const productListParams: Path2Params['/product/list'] = { type: 'electronics', pageSize: '10', pageNo: '1' };

const orderUrl = buildStringWithParams('/order/detail', orderParams);
const productListUrl = buildStringWithParams('/product/list', productListParams);


orderUrl被推断成为/order/detail?orderId=${string},productListUrl被推断成为联合类型了,而我希望函数能直接正确推断出来结果,所以怎么改?

buildStringWithParams函数能够不在运行的情况下就能推断出正确的结果
orderUrl为/order/detail?orderId=123
productListUrl为/product/list?type=electronics&pageSize=10&pageNo=1

共有4个答案

余歌者
2024-07-26
interface Path2Params {
  '/order/detail': { orderId: string }
  '/product/list': { type: string, pageSize: string, pageNo: string }
}

type BuildQueryString<TParams extends Record<string, string>> = {
  [K in keyof TParams]: `${Extract<K, string>}=${TParams[K]}`;
}[keyof TParams];

// ---
type UnionToIntersection<T> = (T extends any ? (args: T) => any : never) extends (args: infer R) => any ? R : never;

type LastInUnion<T> = UnionToIntersection<
  (T extends any ? (arg: T) => any : never)
> extends (arg: infer R) => any ? R : never;

type UnionToTuple<T, U = T> = [T] extends [never] ? [] : [LastInUnion<T>, ...UnionToTuple<Exclude<U, LastInUnion<T>>>];

// 连接元组中的字符串,最后一个元素不添加 '&'
type Join<T extends string[]> =
  T extends []
    ? ''
    : T extends [infer F]
      ? F extends string
        ? `${F}`
        : never
      : T extends [infer F, ...infer R]
        ? F extends string
          ? R extends string[]
            ? `${F}&${Join<R>}`
            : never
          : never
        : never;

type FullUrl<
  TPath extends string,
  TParams extends Record<string, string>,
> = `${TPath}${TParams extends Record<string, never> ? '' : '?'}${
  Join<
    UnionToTuple<
      BuildQueryString<
        TParams
      >
    > extends string[] ? UnionToTuple<
        BuildQueryString<
          TParams
        >
      > : never
  >
}`;

// ---

/**
 * 构建一个带有查询参数的URL字符串。
 * @param path 路径参数,必须是`Path2Params`类型的键。
 * @param params 查询参数,根据路径参数`path`在`Path2Params`中定义的类型。
 * @returns 返回一个完整的URL字符串,包括路径和编码后的查询参数。
 * @template TPath `Path2Params`中的键类型,用于确保类型安全。
 * @template TParams `Path2Params`中由`TPath`指定的键对应的值的类型,用于确保类型安全。
 */
function buildStringWithParams<TPath extends keyof Path2Params, TParams extends Path2Params[TPath]>(
  path: TPath,
  params: TParams,
): FullUrl<TPath, TParams> {
  // 优化:使用数组和join来减少字符串操作的开销
  const encodedParams = Object.entries(params).map(([key, value]) => {
    // 安全性优化:确保value经过适当的编码
    const encodedValue = encodeURIComponent(value);
    // 类型安全优化:不再需要as string,因为value已经被编码为字符串
    return `${encodeURIComponent(key)}=${encodedValue}`;
  });
  const queryString = encodedParams.join('&');
  return `${path}${queryString
    ? '?'
    : ''}${queryString}` as FullUrl<TPath, TParams>;
}

// ↓ ↓ ↓ `as const`
const orderParams = { orderId: '123' } as const satisfies Path2Params['/order/detail'];
const productListParams = { type: 'electronics', pageSize: '10', pageNo: '1' } as const satisfies Path2Params['/product/list'];

const orderUrl = buildStringWithParams('/order/detail', orderParams);
const productListUrl = buildStringWithParams('/product/list', productListParams);

image.png

龙毅
2024-07-26
type UnionToIntersection<U> = (U extends any ? (k: U) => void : never) extends (k: infer I) => void
  ? I
  : never;

type UnionToTuple<U> =
  UnionToIntersection<U extends any ? () => U : never> extends () => infer R
    ? [...UnionToTuple<Exclude<U, R>>, R]
    : [];

type JoinWithAmpersand<T extends any[]> = T extends [
  infer First extends string,
  ...infer Rest extends string[],
]
  ? Rest extends []
    ? First
    : `${First}&${JoinWithAmpersand<Rest>}`
  : "";

type Path2Params = {
  "/order/detail": { orderId: string };
  "/product/list": { type: string; pageSize: string; pageNo: string };
};

type BuildQueryString<TParams extends Record<string, string>> = {
  [K in keyof TParams]: `${Extract<K, string>}=${TParams[K]}`;
}[keyof TParams];


type FinalBuildQueryString<T extends Record<string, string>> = JoinWithAmpersand<
  UnionToTuple<BuildQueryString<T>>
>;


type FullUrl<
  TPath extends string,
  TParams extends Record<string, string>,
> = `${TPath}${TParams extends Record<string, string> ? `?${FinalBuildQueryString<TParams>}` : ""}`;
// ......
const orderUrl = buildStringWithParams("/order/detail", orderParams);
const productListUrl = buildStringWithParams("/product/list", productListParams);
姚雅珺
2024-07-26

首先需要一些 TS 工具类型:

// UnionToIntersection<{ a: 1 } | { b: 2 }> => { a: 1 } & { b: 2 }
type UnionToIntersection<U> = (
    U extends unknown ? (k: U) => void : never
) extends (k: infer I) => void
    ? I
    : never

// LastOfUnion<1 | 2 | 3> => 3
type LastOfUnion<T> = UnionToIntersection<
    T extends unknown ? () => T : never
> extends () => infer R
    ? R
    : never

//! 注意:顺序不一致
// UnionToTuple<1 | 2 | 3> => [2, 1, 3]
type UnionToTuple<
    T,
    L = LastOfUnion<T>,
    N = [T] extends [never] ? true : false
> = true extends N ? [] : Push<UnionToTuple<Exclude<T, L>>, L>

type Push<T extends unknown[], V> = [...T, V]

type Join<T, Separator extends string = ','> = T extends [
    infer F extends string,
    ...infer R extends string[]
]
    ? R extends []
        ? `${F}`
        : `${F}${Separator}${Join<R, Separator>}`
    : never

// JoinUnion<'1' | '2' | '3'> => '1,2,3'
type JoinUnion<T, Separator extends string = ','> = Join<
    UnionToTuple<T>,
    Separator
>

然后就可以开始了:

enum Path {
    order_detail = '/order/detail',
    product_list = '/product/list'
}

type ParamsMap = {
    [K in Path]: {
        [Path.order_detail]: {
            orderId: string
        }
        [Path.product_list]: {
            type: string
            pageSize: string
            pageNo: string
        }
    }[K]
}

type BuildQuery<TParams> = JoinUnion<
    {
        [K in keyof TParams]: `${Extract<K, string>}=${Extract<TParams[K], string>}`
    }[keyof TParams],
    '&'
>

declare function buildURL<
    TPath extends Path,
    TParams extends ParamsMap[TPath],
    TQuery extends BuildQuery<TParams>
>(
    path: TPath,
    params: TParams
): `${TPath}${TQuery extends '' ? '' : `?${TQuery}`}`

// 注意需要使用 <const> 才能正确推断
// a: "/product/list?type=t1&pageSize=2&pageNo=1"
const a = buildURL(Path.product_list, <const>{
    type: 't1',
    pageSize: '2',
    pageNo: '1'
})
仅供学习使用,不建议在生产中这样做

参考:

  • StackOverflow - String Union to string Array
  • StackOverflow - How to transform union type to tuple type
朱阳曜
2024-07-26

在TypeScript中,要实现在编译时就能完全推断出URL字符串的完整形式,你需要确保类型系统能够处理这种动态拼接的场景。然而,由于TypeScript的类型系统主要是静态的,它不能直接处理运行时数据的具体值。不过,我们可以通过一些技巧和泛型约束来尽可能地模拟这种行为。

在你的例子中,主要问题出现在FullUrl类型推断上,因为BuildQueryString<TParams>生成的是一个字符串的联合类型,而不是具体的字符串字面量。要解决这个问题,我们可以使用模板字面量类型(Template Literal Types),这是TypeScript 4.1及以上版本引入的功能。

下面是修改后的代码,使用模板字面量类型来更精确地表示URL的查询字符串部分:

type Path2Params = {
  '/order/detail': { orderId: string };
  '/product/list': { type: string; pageSize: string; pageNo: string };
};

type QueryParam = [key: string, value: string];

// 使用模板字面量类型构建查询字符串
type BuildQueryString<TParams extends Record<string, string>> = {
  [K in keyof TParams as `${string & K}=${TParams[K]}`]: '';
}[keyof TParams];

// 合并路径和查询字符串
type FullUrl<TPath extends keyof Path2Params, TParams extends Path2Params[TPath]> = 
  `${TPath}${'' extends BuildQueryString<TParams> ? '' : '?'}${BuildQueryString<TParams>}`;

function buildStringWithParams<TPath extends keyof Path2Params, TParams extends Path2Params[TPath]>(
  path: TPath,
  params: TParams
): FullUrl<TPath, TParams> {
  const encodedParams = Object.entries(params).map(([key, value]) => 
    `${encodeURIComponent(key)}=${encodeURIComponent(value)}`
  );
  const queryString = encodedParams.join('&');
  return `${path}${queryString ? '?' : ''}${queryString}` as FullUrl<TPath, TParams>;
}

const orderParams: Path2Params['/order/detail'] = { orderId: '123' };
const productListParams: Path2Params['/product/list'] = { type: 'electronics', pageSize: '10', pageNo: '1' };

const orderUrl = buildStringWithParams('/order/detail', orderParams);
const productListUrl = buildStringWithParams('/product/list', productListParams);

// 在IDE中查看类型,应该能得到具体的URL字符串类型
// orderUrl: "/order/detail?orderId=123"
// productListUrl: "/product/list?type=electronics&pageSize=10&pageNo=1"

注意,这里我们使用了[K in keyof TParams as ${string & K}=${TParams[K]}]来映射键和值到一个新的字符串类型,这样BuildQueryString就能生成一个包含所有查询参数的字符串字面量类型的联合。然而,由于TypeScript的类型推断限制,它可能不会在IDE中直接显示为完全展开的字符串,但在大多数情况下,这足以让TypeScript的类型检查器在编译时捕捉到错误。

另外,请注意,虽然这种方法在类型层面上尽可能接近你的需求,但TypeScript的类型系统仍然是静态的,它不能保证在运行时生成的字符串与类型定义完全一致(例如,如果查询参数的值包含特殊字符或未正确编码)。因此,你仍然需要在运行时进行必要的检查和编码。

 类似资料:
  • 大家好,我有这样的问题: 它说: 隐式类必须有一个主构造函数,并且在def traverseFilteringErrors的第一个参数列表中只有一个参数 和 类型不匹配。必填:Future[B] = 我是新来的scala,所以我应该怎么做来解决这个问题?

  • 各位大佬,我想请问一下我想通过一个数组对象处理另外一个数组对象。生成一个新的数组对象要怎么处理?我想通过两个时间的字段做判断,给新生成的数组对象里面属性的值加上一个1。怎么写都不对。生成的都是有问题的。 通过的数组对象 想要处理的数组对象 我的思路有问题想不出来了,写的代码不对生成的有问题。 我希望的是生成新的数组对象,合并同名,并且根据thisDate生成新的属性。在和data这个json判断时

  • 我有一系列复杂的类型级别函数,它们的计算结果如下: 显然,在这种情况下,这个表达式是一个。更一般地说,我们可以说: 有没有办法教GHC推断这一点? 编辑:@chi指出,在某些情况下,GADT可以解决这一问题,但我的特殊情况是: 然后 不能被访问,但是也许GHC应该能够推断出

  • 我有一个非常奇怪的问题,很简单,但我不明白问题是什么。 我有一个类,ClassA调用ClassB中的函数,比如- 类A是在我的applicationContext中定义的bean。类xml ClassB中的函数定义看起来像 IntelliJ没有指出任何语法问题,一切看起来都很正常。。。然而,当我试图编译时,Maven出现了一个异常 B类与a类位于不同的模块中,因此B类位于a类的pom中。作为依赖项

  • 我试着看文档,但不明白第二个论点是什么。我知道。sort(array,1,4)表示从索引1到3进行排序。但是在这种情况下,带有<代码>的箭头-

  • 问题内容: 什么是参数: 如果我这样做: 并具有两个Localizable.strings版本(英语和西班牙语),每个版本是否都需要输入: 英语难道不是多余的吗? 问题答案: 注释字符串被应用程序忽略。它用于翻译者的利益,可在您的应用程序中找到的键的上下文用法中添加含义。 例如,键在给定语言中的取值可能不同,具体取决于该短语在该语言中需要使用的正式或非正式程度(“ Whats up World”,

  • 问题内容: 在Swift 2中,调用函数时似乎并不总是需要第一个参数名称。现在在Swift 3中,调用函数时需要第一个参数名称。例如: Swift 2.2允许通过简单键入以下内容来调用该函数: Swift 3似乎要求我们使用方法的第一个参数名称,例如: Swift 3是否适用于所有函数和方法,还是仅适用于某些情况? 问题答案: 是的,这是正确的。Swift正以这种方式解决语言不一致的问题(初始化程

  • 本文向大家介绍javascript 获取函数形参个数,包括了javascript 获取函数形参个数的使用技巧和注意事项,需要的朋友参考一下