扩展运算符和可选字段。如何推断正确的类型
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<keyof T2, keyof T1>
- 仅使用不在第一个对象中的键
【讨论】:
太棒了!适用于第一种情况: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,请通过创建工厂来限制其使用:<T>createPartial (o: Partial<T>) => Undefinable<T>
。这样createPartial<MyTypeNonOptional>()
的结果就会被推断为 value: undefined
或Undefinable<MyTypeNonOptional>
。
如果您需要将字段值设为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
类型表达后者。
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 投票和/或在这些边缘情况下使用解决方法(演员、函数)。以上是关于扩展运算符和可选字段。如何推断正确的类型的主要内容,如果未能解决你的问题,请参考以下文章