我不知道该怎么调用它,所以在我描述它时请裸露我的。
我有三个配置对象:
const configA = {
type: 'A' as const,
getPath: (query: { foo: string }) => `/${query.foo}`
}
const configB = {
type: 'B' as const,
getPath: (query: {}) => `/path/b`
}
const configC = {
type: 'C' as const,
getPath: (query: {bar: number}) => `/c/${query.bar}`
}
我将这些合并到一个主记录中,如下所示:
const CONFIGS = {
[configA.type]: configA,
[configB.type]: configB,
[configC.type]: configC
}
我现在想编写一个函数,其参数遵循上面的配置。这样就提供了类型安全。因此,如果我这样做的话,上面的内容是:
add({
type: 'C',
query: { foo: true }
})
Typescript 应该警告我,如果 type
是 C
,则此处的 query
对象无效。然而它并没有警告我。
我尝试过这个:
function add(inputs: {
type: (typeof CONFIGS)[keyof typeof CONFIGS]['type'],
query: Parameters<(typeof CONFIGS)[keyof typeof CONFIGS]['getPath']>[0]
}) {
const config = CONFIGS[inputs.type];
if (!config) {
throw new Error('Config not found');
}
const url = 'mysite.blah' + config.getPath(inputs.query); // inputs.query is getting Typescript warning
console.log('url:', url);
}
但是它不起作用,并且还有一个额外的问题,即我执行 config.getPath(inputs.query)
的行上的 inputs.query
抛出此 Typescript错误:
Argument of type '{} | { foo: string; } | { bar: number; }' is not assignable to parameter of type '{ foo: string; } & { bar: number; }'.
这是我的完整代码:TypeScript Playground
这种技术叫什么?有可能解决这个问题吗?谢谢。
最佳答案
add
的调用签名不是类型安全的,因为 inputs
的 type
和 query
属性参数之间是相互独立的。两者都是 union types其中每个都可以是(三个)可能值中的任何一个。您可以通过将inputs
的类型设置为单个联合来解决此问题,以便type
和query
相互关联:
type Inputs = {
[P in keyof typeof CONFIGS]: {
type: P,
query: Parameters<typeof CONFIGS[P]["getPath"]>[0]
}
}[keyof typeof CONFIGS]
/* type Inputs =
{ type: "A"; query: { foo: string; }; } |
{ type: "B"; query: {}; } |
{ type: "C"; query: { bar: number; }; } */
declare function add(inputs: Inputs): void;
add({ type: 'C', query: { foo: true } }); // error!
add({ type: 'C', query: { bar: 123 } }); // okay
这从调用者的角度解决了您的问题。不幸的是,实现本质上有相同的错误:
function add(inputs: Inputs) {
const config = CONFIGS[inputs.type];
if (!config) {
throw new Error('Config not found');
}
const url = 'mysite.blah' + config.getPath(inputs.query); // error!
console.log('url:', url);
};
这是因为 TypeScript 缺乏对我所说的“相关联合”的直接支持,如 microsoft/TypeScript#30581 中所述。 。当编译器类型检查单个代码块(例如 add()
主体)时,它会执行一次。仅从体内表达式的类型来看,没有足够的信息来验证您所做的事情是否安全。是的,inputs
必须是正确的类型,但是当您尝试使用它两次(例如 CONFIGS[inputs.type].getPath(inputs.query)
)时,编译器无法跟随线程。如果它可以分析该代码三次,每次联合缩小一次,它就会起作用。但它只能执行一次。
microsoft/TypeScript#47109 中描述了处理此问题的推荐方法。并涉及重构,以便用简单的键值类型和 generic 来表达操作,而不是联合。 indexes进入该类型并进入 mapped types超过那种类型。目标是使 CONFIGS[input.type].getPath
被视为单个函数(而不是联合),其参数与 input.query< 的参数具有相同的泛型类型
.
重构可能如下所示:
首先让我们重命名 CONFIGS
,因为我们需要更改它的类型:
const _CONFIGS = {
[configA.type]: configA,
[configB.type]: configB,
[configC.type]: configC
}
然后我们将使用它来构建我们的简单键值类型:
type QueryMap = { [K in keyof typeof _CONFIGS]:
Parameters<typeof _CONFIGS[K]["getPath"]>[0]
}
/* type QueryMap = {
A: { foo: string; };
B: {};
C: { bar: number;};
} */
我们将使用 QueryMap
类型作为此处涉及的关系的基础。您可以从手动编写的类型开始,并根据它定义 CONFIGS
,但这更容易扩展,因为如果您添加成员,它会自动更新。
现在我们将 CONFIGS
的类型重写为 QueryMap
上的映射类型:
type Configs = { [K in keyof QueryMap]: { type: K, getPath: (query: QueryMap[K]) => string } };
并说 CONFIGS
属于这种类型:
const CONFIGS: Configs = _CONFIGS; // okay
赋值成功,因此编译器能够看到typeof _CONFIGS
和Configs
之间的等价性。现在我们可以将 add()
重写为 K extends keyof Configs
中的通用函数:
function add<K extends keyof Configs>(inputs: { type: K, query: QueryMap[K] }) {
const config = CONFIGS[inputs.type];
if (!config) {
throw new Error('Config not found');
}
const url = 'mysite.blah' + config.getPath(inputs.query); // okay
console.log('url:', url);
}
现在可以了。这很微妙,因为如果您在函数第一行中将 CONFIGS
替换为 _CONFIGS
,错误将会再次出现。是的,它们的类型是等效的,但编译器对它们的处理方式不同。 Configs
类型表示为 QueryMap
上的映射类型,并且 inputs
也具有依赖于 QueryMap
的类型,这个对应关系可用于类型检查功能。 _CONFIGS
的类型未针对 QueryMap
进行表达,因此编译器看不到它与输入
之间的通用连接。
哦,调用者仍然会得到您想要的行为:
add({ type: 'C', query: { foo: true } }); // error!
add({ type: 'C', query: { bar: 123 } }); // okay
给你。此重构与 add()
实现的编译器验证的类型安全性非常接近。我不确定这是否一定值得;如果您不关心编译器在实现中检查您的代码,您可以使用 type assertion使其静音:
function add(inputs: Inputs) {
const config = CONFIGS[inputs.type];
if (!config) {
throw new Error('Config not found');
}
const url = 'mysite.blah' + config.getPath(inputs.query as any); // okay
console.log('url:', url);
};
这更容易做到。这完全取决于您的用例。
关于typescript - 基于配置对象智能强制参数类型,我们在Stack Overflow上找到一个类似的问题: https://stackoverflow.com/questions/76239187/