我很好奇const { liveCount } = customer
为什么不能在以下方面工作。tsc抛出Property 'liveCount' does not exist on type 'Customer'.(2339)
错误。它们之间有什么区别,我需要了解更多才能理解这一点?
使用TS 4.2.3。
type Thing = {
name: string
}
type Person = Thing & {
age: number
address?: string
}
type Cat = Thing & {
liveCount: number
}
type Customer = Person | Cat
function deliverTo(customer: Customer) {
// Case A: OK
const { liveCount } = { ...customer }
// Case B: Not OK
// const { liveCount } = customer
}
您只能访问联合的所有成员上存在的属性
以下行为符合预期:
declare const customer: Customer;
const livecount = customer.liveCount; // error!
// Property 'liveCount' does not exist on type 'Customer'
// Property 'liveCount' does not exist on type 'Person'.
TypeScript只允许您访问联合类型值的属性键,前提是该属性键已知存在于联合的每个成员中。参见";使用并集类型";在TS手册中。在上文中,您无法访问customer.liveCount
,因为已知Person
类型的值上不存在名为"liveCount"
的属性键。
因此,对于您示例中的所有代码,我们应该预料到Customer
上不存在liveCount
的错误。事实上,你的";情况A";,const { liveCount } = { ...customer }
恰好没有导致错误,这显然是TypeScript中的一个错误。请参阅我提交的关于这个问题的microsoft/TypeScript#43174。至少到今天(2021年3月10日),TypeScript 4.3.1的错误已经修复,所以如果在不久的将来,你会在两个";情况A";以及";情况B";。
如果你在想";等等,customer.liveCount
不是number | undefined
类型吗&";,这可能是因为你的思维过程是这样进行的:
customer
是Cat
或Person
。✔- 如果
customer
是Cat
,则customer.liveCount
将是number
。✔ - 如果
customer
是Person
,则customer
将根本不具有liveCount
属性。❌ - 如果
customer
不具有liveCount
属性,则customer.liveCount
将是undefined
。✔ - 因此CCD_ 26将是CCD_ 27或CCD_ 28。❌
- 因此
customer.liveCount
具有number | undefined
类型。❌
用复选标记标记的语句(✔)是正确的,但标有十字记号的(❌)对于TypeScript不正确。让我们关注一下这第一次误入歧途的地方:
- 如果
customer
是Person
,则customer
将根本不具有liveCount
属性。❌
这不是真的。TypeScript中的对象类型是打开或可扩展。可以在不违反原始类型的情况下向对象类型添加属性。这是一件好事,因为它使接口和类扩展能够形成类型层次结构。如果bar
是类型Bar
,并且Bar extends Foo
,则bar
也是类型Foo
。但它也有一些令人讨厌的含义,就像我们在这里看到的那样。
已知类型为Person
的值具有:一个名为name
的属性,其类型是string
;名为CCD_ 43的属性,其类型为number
;以及可选地,名为address
的属性,如果存在,则具有类型string | undefined
。但是Person
类型的值不知道缺少任何其他属性。具体而言,尚不清楚是否缺少名为liveCount
的属性。而且,在这个关键点上可能存在什么样的属性是没有限制的。它可能是number
、string
、Date
或任何东西。唯一可以肯定的是,它类似于unknown
或any
。所以我们必须把我们的逻辑修改成这样:
customer
是Cat
或Person
。✔- 如果
customer
是Cat
,则customer.liveCount
将是number
。✔ - 如果
customer
是Person
,则customer
可以具有liveCount
属性,其类型可以是任何类型。✔ - 那么
customer.liveCount
将是any
。✔ - 所以
customer.liveCount
将是number
或any
。✔ - 因此,
customer.liveCount
的类型为any
,首先访问customer
上的liveCount
属性可能是错误的。✔
示例:
const undeadCount = {
name: "Dracula",
age: 1000,
liveCount: false
}
const weirdPerson: Person = undeadCount;
deliverTo(undeadCount);
这里,undeadCount
是有效的Person
,因为它具有正确类型的name
和age
。它具有boolean
类型的liveCount
属性,但这并不能取消它作为Person
的资格,正如我可以将它毫无错误地分配给Person
类型的变量weirdPerson
这一事实所证明的那样。所以我也可以打电话给deliverTo(undeadCount)
。如果deliverTo()
的实现期望customer.liveCount
是number
,如果它不是undefined
,那么可能会发生一些非常奇怪的事情。您甚至可能出现运行时错误(例如customer.liveCount?.toFixed(2)
)。
注意,如果你有一种说";Customer
要么是精确地Cat
,要么确切地Person
,其中在这两种情况下都不可能存在我不知道的额外性质";。但是TypeScript目前不支持所谓的";确切类型";,并且存在在microsoft/TypeScript#12936上实现它们的长期功能请求。那么,如果没有一个合理的方式来说Exact<Cat> | Exact<Person>
,该怎么办呢?
如何继续
有不同的解决方法。您可以做的一件事是断言您的customer
具有可选的liveCount
属性,该属性是number
或undefined
。当然,从技术上讲,有人可能会通过undeadCount
,但你真的没想到会发生这种情况,你也不想浪费时间担心这样一个不太可能的事件。一个这样的断言是:
const { liveCount } = customer as Partial<Cat>; // okay
这里我们说customer
将具有来自Cat
的属性,或者它们将丢失,从而丢失undefined
。这包括liveCount
。
这个变通方法的一个更详细的版本是说customer
是这样定义的ExclusiveCustomer
:
type ExclusiveCustomer = {
name: string;
age: number;
address?: string | undefined;
liveCount?: undefined;
} | {
name: string;
liveCount: number;
age?: undefined;
address?: undefined;
};
您明确地说,如果customer
是Person
,那么它就缺少任何特定于Cat
的属性,反之亦然。您甚至可以让编译器通过ExclusiveUnion
这样的类型函数从Customer
类型计算ExclusiveCustomer
,这是另一个SO问题:
const { liveCount } = customer as ExclusiveUnion<Customer>;
另一种解决方法是使用缩小范围,通过检查customer
的内容,将其视为Person
或Cat
。最简单的方法是,从表面上看,与关于开放类型的答案的第一部分完全不一致:
const liveCount = "liveCount" in customer ? customer.liveCount : undefined;
// const liveCount: number | undefined
什么?是的,您可以使用in
运算符作为类型保护,将customer
从Customer
缩小到Cat
(如果"liveCount" in customer
为true)或Person
(如果为false)。这种类型保护是在microsoft/TypeScript#115256中实现的,其中的相关评论暗示人们需要某种方式来做到这一点,这充分表明了我们会让他们这样做的意图,即使这在技术上是不安全的:
现实是,大多数工会已经正确地脱节,并且没有足够的别名来表明问题。写CCD_ 119测试的人不会写";"更好";检查在实践中真正发生的是,人们添加类型断言,或者将代码移动到同样不健全的用户定义类型谓词。在网上,我不认为这比现状更糟(而且更好,因为它引入了更少的用户定义类型谓词,如果使用
in
编写,由于缺乏拼写错误检查,这些谓词很容易出错)。
这导致了下一种方法:通过返回类型谓词的函数编写自己的类型保护。
function isCat(x: any): x is Cat {
return x && typeof x.liveCount === "number" && typeof x.name === "string"; 😸
}
const liveCount = isCat(customer) ? customer.liveCount : undefined;
// const liveCount: number | undefined
通过编写返回x is Cat
的isCat()
,我们告诉编译器允许使用isCat()
将某个内容缩小为Cat
(如果为true)或非Cat
(如果为false)。如果您呼叫isCat(customer)
,则表示Cat
或Person
。我碰巧以最安全的方式实现了isCat()
,但我承担了这个责任,编译器放弃了它
function isCat(x: any): x is Cat {
return Math.random() < 0.5 // 🙀
}
编译器也不会更聪明。好了。
RECAP
- 您只能安全地访问联合的所有成员上存在的属性
- 你在它工作的地方看到的行为是一个bug。不要指望它
- 如果假设TypeScript中的类型是精确的,那么可能会发生奇怪的坏事
- 但是它们可能不会发生,所以您可以使用类型断言来使编译器停止抱怨
- 或者,您可以使用类型窄化使编译器停止抱怨,并具有不同程度的安全性
到代码的游乐场链接