假设我有这个例子:

type Keys = 'one' | 'two';
type Values<T extends Keys> = T extends 'one' 
  ? { id: 1, name: 'hello1' } 
  : T extends 'two' 
    ? { id: 2, value: 'hello2' }
      : never;

type Fn = <T extends Keys>(key: T) => (value: Values<T>) => void;

const fn: Fn = (key) => (value) => {
  if (key === 'one') {
    const name = value.name; // This throws compile error
  }
}

// Both of these `fn` calls correctly limit the `value` argument based on the `key`
fn('one')({ id: 1, name: 'hello1' });
fn('two')({ id: 2, value: 'hello2' });

有人能向我解释一下,在调用fn方法时,这个设置correctly limits允许value参数的类型,但不允许我在fn实现中使用类型保护后缩小value的范围吗?

Playground-我有这个playground ,还有其他几次try ,但都没有成功.

最终,我try 在Reaction项目中使用与此类似的方法作为useCallback方法,在该项目中,onChange属性设置为使用键调用的回调,然后事件发送值.既然我知道活动的类型,我觉得这应该是可能的.例:

<Component1 onChange={handleChange('key1')} />
<Component2 onChange={handleChange('key2')} />

推荐答案

首先,让我们省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来实现,这样就可以防止人们传入Kunion,因为这会搞砸事情.还有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()是同时接受kv的单个函数,而不是使用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只允许安全呼叫;如果你不匹配kv,它会投诉.但是现在实现主体可以继续使用kv作为非 struct 化区分联合;判断kv被缩小.


这就对了.事实上,一切都是安全的(或者说是合理的安全).泛型有时会被不安全地使用),并且编译器不会抱怨.同样,诀窍在于这对调用和实现签名:

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

Typescript相关问答推荐

条件类型中的Typescript二元组推理

物料UI图表仅在悬停后加载行

如何将绑定到实例的类方法复制到类型脚本中的普通对象?

有没有办法适当缩小类型?

替代语法/逻辑以避免TS变量被分配之前使用." "

rxjs forkJoin在错误后不会停止后续请求

如何在单击停止录制按钮后停用摄像机?(使用React.js和Reaction-Media-Recorder)

如何推断计算逻辑的类型

ANGLE找不到辅助‘路由出口’的路由路径

使用TypeScrip根据(可选)属性推断结果类型

从子类集合实例化类,同时保持对Typescript中静态成员的访问

TypeScrip-根据一个参数值验证另一个参数值

TS2739将接口类型变量赋值给具体变量

map 未显示控制台未显示任何错误@reaction-google-map/api

在Google授权后由客户端接收令牌

作为参数的KeyOf变量的类型

基于区分的联合键的回调参数缩小

使用NextJS中间件重定向,特定页面除外

如何避免TS2322;类型any不可分配给类型never;使用索引访问时

显式推断对象为只读