扩展运算符和可选字段。如何推断正确的类型

Posted

技术标签:

【中文标题】扩展运算符和可选字段。如何推断正确的类型【英文标题】:Spread operator and optional fields. How to infer proper type 【发布时间】:2020-06-22 21:28:32 【问题描述】:

假设您有一个具有不可为空的必填字段的对象:

interface MyTypeRequired  
    value: number;

你想用另一个对象的字段来更新它,用一个可选字段:

interface MyTypeOptional  
    value?: number;

所以你继续创建一个函数:

function createObject(a: MyTypeRequired, b: MyTypeOptional) 
    return  ...a, ...b ;

这个函数的推断返回类型是什么?

const a = createObject( value: 1 ,  value: undefined );

实验表明它符合 MyTypeRequired 接口,即使第二个扩展有一个可选字段。

如果我们改变顺序,推断的类型不会改变,即使运行时类型会不同。

function createObject(a: MyTypeRequired, b: MyTypeOptional) 
    return  ...b, ...a ;

TypeScript 为什么会有这样的行为以及如何解决这个问题?

【问题讨论】:

我认为推断的类型应该是这样的: value?: number: undefined for ...a, ...b 而这个: value: number for ...b, ...a 发现相关问题:github.com/microsoft/TypeScript/issues/13195 你让我在这里,我认为这是微不足道的,但现在看到问题了 看起来正在为number | undefined工作:typescriptlang.org/play/… 【参考方案1】:

解决方法可以通过类型级函数来完成。考虑:

interface MyTypeRequired  
    a: number;
    value: number;


interface MyTypeOptional  
    b: string;
    value?: number;


type MergedWithOptional<T1, T2> = 
    [K in keyof T1]: K extends keyof T2 ? T2[K] extends undefined ? undefined : T2[K] : T1[K] 
 & 
    [K in Exclude<keyof T2, keyof T1>]: T2[K]


function createObject(a: MyTypeRequired, b: MyTypeOptional): MergedWithOptional<MyTypeRequired, MyTypeOptional> 
    return  ...b, ...a ;

我添加了其他字段以检查行为。我正在做的是,当第二个对象具有可选字段(可能是undefined)时,请考虑将其添加到联合中,因此结果将是T1[K] | undefined。第二部分只是合并 T2 中但不在 T1 中的所有其他字段。

更多细节:

K extends keyof T2 ? T2[K] extends undefined ? undefined : T2[K] : T1[K] 如果第二个对象有这个键并且它的值可以是未定义的,那么将 undefined 附加到值类型,它被附加而不是替换,因为这就是 conditional types behaves for union types Exclude&lt;keyof T2, keyof T1&gt; - 仅使用不在第一个对象中的键

【讨论】:

太棒了!适用于第一种情况:cutt.ly/btafI9N 但不适用于另一种情况。也许没关系。 请检查我的答案并判断它是否合理。 嗨@Vanuan 其他情况是什么意思? value: undefined value: number 合并时。保证结果为 value: number 。也就是说createObject(a: MyTypeOptional, b: MyTypeRequired): 将返回MyTypeOptional,而它应该返回MyTypeRequired。所以它适用于第一个订单,但不适用于第二个订单。你点击链接了吗? shouldSucceed 变量不应该有错误【参考方案2】:

警告!这是我对发生的事情的解释(理论)。尚未确认。


问题似乎是处理可选字段的预期行为。

当你定义这个类型时:

interface MyTypeOptional  
    value?: number;

TypeScript 期望该值的类型为 number | never

所以当你传播它时

const withOptional: MyTypeOptional = ;
const spread =  ...withOptional ;

传播对象的类型应该是 value: never | number value?: number

不幸的是,由于可选字段与未定义的使用不明确,TypeScript 会这样推断传播对象:

value?: number | undefined

所以可选字段类型在传播时被强制转换为number | undefined

没有意义的是,当我们传播非可选类型时,它似乎覆盖了可选类型:

interface MyTypeNonOptional  
    value: number;

const nonOptional =  value: 1 ;
const spread2 =  ...nonOptional, ...withOptional 

看起来像一个错误?但是不,这里 TypeScript 不会将可选字段转换为 number | undefined。在这里它将它转换为number | never

我们可以通过将可选更改为显式|undefined 来确认这一点:

interface MyTypeOptional  
    value: number | undefined;

这里,展开如下:

const withOptional: MyTypeOptional =  as MyTypeOptional;
const nonOptional =  value: 1 ;
const spread2 =  ...nonOptional, ...withOptional 

会被推断为 value: number | undefined;

而当我们将其更改为可选或未定义时:

interface MyTypeOptional  
    value?: number | undefined;

又坏了,推断为 value: number


那么解决方法是什么?

我想说,在 TypeScript 团队修复它们之前不要使用可选字段。

这有几个含义:

如果需要在未定义的情况下省略对象的键,请使用omitUndefined(object) 如果您需要使用 Partial,请通过创建工厂来限制其使用:&lt;T&gt;createPartial (o: Partial&lt;T&gt;) =&gt; Undefinable&lt;T&gt;。这样createPartial&lt;MyTypeNonOptional&gt;() 的结果就会被推断为 value: undefined Undefinable&lt;MyTypeNonOptional&gt;。 如果您需要将字段值设为unset,请使用null

关于可选字段的这种限制存在一个未解决的问题: https://github.com/microsoft/TypeScript/issues/13195

【讨论】:

【参考方案3】:

对象传播类型被解析as follows:

调用和构造签名被剥离,只保留非方法属性,对于具有同名属性最右边的属性的类型是使用过

带有通用展开表达式的对象文字现在产生交集类型,类似于 Object.assign 函数和 JSX 文字。例如:

这很好用,直到您为最右边的参数设置了一个可选属性。 value?: number; 可以表示 1) 缺少属性或 2) 具有 undefined 值的属性 - TS cannot distinguish 这两种情况带有可选的修饰符 ? 表示法。举个例子:

const t1:  a: number  =  a: 3 
const u1:  a?: string  =  a: undefined 

const spread1 =  ...u1  //  a?: string | undefined; 
const spread2 =  ...t1, ...u1  //  a: string | number; 
const spread3 =  ...u1, ...t1  //  a: number; 
传播1

有道理,属性键a可以设置也可以不设置。我们别无选择,只能用undefined 类型表达后者。

传播2

a 的类型由最右边的参数决定。因此,如果u1 中的a 存在,它将是string,否则扩展运算符将只从第一个参数t1 中获取a,类型为number。所以string | number 也有点道理。 注意:这里没有undefined。 TS 假定,该属性根本不存在,或者它是一个string。如果我们给a 一个明确的属性值类型undefined,结果会有所不同:

const u2 =  a: undefined 
const spread4 =  ...t1, ...u2  //  a: undefined; 
传播3

最后一个很简单:t1 中的a 覆盖u1 中的a,所以我们得到了number


我wouldn't bet on a fix,所以这是一个可能的解决方法,使用单独的Spread 类型和函数:

type Spread<L, R> = Pick<L, Exclude<keyof L, keyof R>> & R;

function spread<T, U>(a: T, b: U): Spread<T, U> 
    return  ...a, ...b ;


const t1_:  a: number; b: string 
const t2_:  a?: number; b: string 
const u1_:  a: number; c: boolean 
const u2_:  a?: number; c: boolean 

const t1u2 = spread(t1_, u2_); //  b: string; a?: number | undefined; c: boolean; 
const t2u1 = spread(t2_, u1_); //  b: string; a: number; c: boolean; 

希望,这是有道理的!以上代码是sample。

【讨论】:

知道了。因此,在这种情况下,传播的结果不能是可选的。如果第一个价差具有所需的值,这是有道理的。 但这意味着不能为可选字段分配可以未定义的值。 同样应该禁止删除非可选字段。这意味着变量的类型永远不会改变。 使用上面的特殊spread 函数,您可以undefined 分配给最右边的对象参数的可选字段(例如a?: number),给定一些其他传播根据需要使用a 的对象参数。结果类型正确输出a?: number | undefined。相反,正常传播会将结果类型的a 标记为a: number(必需)。我不确定您所说的“应禁止类似地删除非可选字段”是什么意思。 您现在能做的最好的事情是给issue 投票和/或在这些边缘情况下使用解决方法(演员、函数)。

以上是关于扩展运算符和可选字段。如何推断正确的类型的主要内容,如果未能解决你的问题,请参考以下文章

C#类详解

三元运算符 '?:' 在 4.9.0 之前的 GCC 版本中推断出不正确的类型? [关闭]

TS1109,WebStorm 对可选链接的反应不正确

空值合并运算符和可选链

空值合并运算符和可选链

PrimeFaces 可扩展和可选行