我不知道该怎么称呼这个,所以请在我描述它的时候与我裸露。
我有三个配置对象:
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错误:
类型“{}”的参数|{ foo:string;}|{ bar:number;}'不能赋值给类型为'{ foo:string; } & { bar:number;}'。
以下是我的完整代码:TypeScript Playground
这种技术叫什么?有可能解决这个问题吗?谢谢。
1条答案
按热度按时间yhuiod9q1#
add
的调用签名不是类型安全的,因为inputs
参数的type
和query
属性彼此独立。两者都是联合类型,每个都可以是(三个)可能值中的任何一个。您可以通过将inputs
的类型设置为单个联合来解决此问题,以便type
和query
相互关联:这从呼叫方的Angular 解决了您的问题。不幸的是,实现基本上有相同的错误:
这是因为TypeScript缺乏对microsoft/TypeScript#30581中描述的“相关联合”的直接支持。当编译器类型检查单个代码块(如
add()
的主体)时,它只执行一次。仅仅从身体内部的表达式的类型来看,没有足够的信息来验证你正在做的事情是安全的。是的,inputs
必须是正确的类型,但是当你像CONFIGS[inputs.type].getPath(inputs.query)
一样尝试使用它两次时,编译器无法跟踪线程。如果它可以分析该代码 * 三次 *,每次联合收缩一次,它就会工作。但它只能这样做一次。处理这个问题的推荐方法在microsoft/TypeScript#47109中有描述,它涉及到重构,这样操作就不是联合,而是用一个简单的键-值类型来表示,然后将genericindexes转换为该类型,再转换为该类型上的mapped types。我们的目标是使
CONFIGS[input.type].getPath
被视为一个函数(而不是联合),其参数与input.query
的参数是相同的泛型类型。重构可能看起来像这样:
首先让我们将
CONFIGS
重新命名,因为我们需要改变它的类型:然后我们将使用它来构建简单的键值类型:
我们将使用
QueryMap
类型作为这里涉及的关系的基础。您可以将其作为手动编写的类型并根据它定义CONFIGS
,但这更容易扩展,因为如果您添加成员,它将自动更新。现在我们将
CONFIGS
的类型重写为QueryMap
上的Map类型:假设
CONFIGS
是这样的类型:赋值成功,因此编译器能够看到
typeof _CONFIGS
和Configs
之间的等价性。现在我们可以将add()
重写为K extends keyof Configs
中的泛型函数:现在这个方法有效了。这很微妙,因为如果在函数的第一行中将
CONFIGS
替换为_CONFIGS
,错误将再次出现。是的,它们的类型是等价的,但编译器并不以相同的方式对待它们。Configs
类型表示为QueryMap
上的Map类型,inputs
也具有依赖于QueryMap
的类型,并且这种对应关系可以用于对函数进行类型检查。_CONFIGS
的类型不是相对于QueryMap
来表示的,所以编译器看不到它和inputs
之间的泛型连接。对了,呼叫者仍然可以获得您想要的行为:
给你对于
add()
实现来说,这种重构是最接近编译器验证的类型安全的。我不确定这是否值得如果你不关心编译器在实现中检查你的代码,你可以只使用类型Assert来沉默它:这就容易多了这一切都取决于您的使用案例。
Playground链接到代码