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
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);
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);
首先需要一些 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'
})
仅供学习使用,不建议在生产中这样做
参考:
在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”,