首先,让我们省go 你对Values<K>
的conditional type定义.这可以很容易地将indexed access重构为映射接口:
interface KeyVals {
one: { id: 1, name: "hello1" },
two: { id: 2, value: "hello2" }
}
type Keys = keyof KeyVals;
type Values<K extends Keys> = KeyVals[K];
编译器在分析索引访问方面比在分析条件类型时稍好一些.这不会解决您的问题,但它确实会让我的解决方法运行得更好,所以我会这么做的.
这里的主要问题是编译器处理discriminated unions的方式和处理generics的方式不匹配.fn()
函数调用签名是泛型的,值k
的类型为K
,值v
的类型为Values<K>
,但您的函数实现需要将它们视为区分的联合,而判断k
的值应该缩小v
的类型.但是勾选k
只缩小了k
的类型,并且确实缩小了类型parameter K
.因此,你不能把这些普遍的价值观视为歧视的结合.
这在某种程度上是目前Tyescrip的一个普遍限制,GitHub中提出了关于它的不同方面的各种问题.例如,有microsoft/TypeScript#33014,它显式地请求判断k
以缩小类型参数K
的范围.这一问题目前尚未解决.它可能还需要microsoft/TypeScript#27808来实现,这样就可以防止人们传入K
的union,因为这会搞砸事情.还有microsoft/TypeScript#30581条询问如何泛化地对待联合,在microsoft/TypeScript#47109中推荐的修复/方法是从本质上将联合重新构建为泛型……但在这里,你need的unions 行为才能奏效.眼下,这似乎是不可能的.
您还try 了将fn()
设置为非通用overloaded function的方法.这将完全避免泛型.不幸的是,与泛型相比,编译器分析重载的能力更差.在重载函数statement的实现内,编译器不将事物限制为仅与调用签名兼容;它只查看实现签名,并且它非常松散地匹配实现和调用签名.有关以类型安全的方式判断重载的拒绝请求,请参见microsoft/TypeScript#13235.如果你使用一个重载函数expression,那么编译器就太strict了.请参见microsoft/TypeScript#47669以获取仍未完成的请求,以在此处执行一些不同的操作.因此,可以进行重载以 suppress 错误(但允许一些不安全的内容)或捕获错误(但阻止一些安全的内容),但不能两者兼而有之.
因此,据我所知,没有办法重构你的代码,以便编译器允许你做正确的事情,同时阻止你做错误的事情.我所能想到的只有一个变通办法.
在这种情况下,我能得到的最接近有用的安全性是使用函数重载语句来弥合泛型和区别联合之间的差距.它看起来是这样的:
function uncurriedFn<K extends Keys>(k: K, v: Values<K>): void;
function uncurriedFn(...[k, v]: { [K in Keys]: [k: K, v: Values<K>] }[Keys]) {
if (k === 'one') {
const name = v.name;
}
}
所以uncurriedFn()
是同时接受k
和v
的单个函数,而不是使用k
并返回接受v
的函数的咖喱版本.该函数的call signature是泛型的,因此您应该能够将其作为泛型函数安全地调用.事实上,这让你可以在你想要的咖喱版本中使用它:
type Fn = <K extends Keys>(k: K) => (v: Values<K>) => void;
const fn: Fn = (k) => (v) => uncurriedFn(k, v)
fn('one')({ id: 1, name: 'hello1' });
fn('two')({ id: 2, value: 'hello2' });
这就是呼叫签名.那implementation号呢?实现签名是非泛型的,参数为destructured discriminanted union.类型{[K in Keys]: [k: K, v: Values<K>]}[Keys]
是计算为[k: 'one', v: Values<'one'>] | [k: 'two', v: Values<'two'>]
的distributive object(如在MS/TS#47implementation中创造的).这个also只允许安全呼叫;如果你不匹配k
和v
,它会投诉.但是现在实现主体可以继续使用k
和v
作为非 struct 化区分联合;判断k
和v
被缩小.
这就对了.事实上,一切都是安全的(或者说是合理的安全).泛型有时会被不安全地使用),并且编译器不会抱怨.同样,诀窍在于这对调用和实现签名:
function uncurriedFn<K extends Keys>(k: K, v: Values<K>): void;
function uncurriedFn(...[k, v]: { [K in Keys]: [k: K, v: Values<K>] }[Keys]) {}
在理想情况下,编译器应该知道签名<K extends Keys>(k: K, v: Values<K>) => void
和(...args: {[K in Keys]: [k: K, v: Values<K>]}[Keys]) => void
是兼容的(模MS/TS#27808).但它不知道这一点,所以您必须利用重载语句判断的松散性来欺骗它.
Playground link to code个