来自tsc(typescript)的类型BBB错误中不存在属性AAA



我很好奇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类型吗&";,这可能是因为你的思维过程是这样进行的:

  • customerCatPerson。✔
  • 如果customerCat,则customer.liveCount将是number。✔
  • 如果customerPerson,则customer将根本不具有liveCount属性。❌
  • 如果customer不具有liveCount属性,则customer.liveCount将是undefined。✔
  • 因此CCD_ 26将是CCD_ 27或CCD_ 28。❌
  • 因此customer.liveCount具有number | undefined类型。❌

用复选标记标记的语句(✔)是正确的,但标有十字记号的(❌)对于TypeScript不正确。让我们关注一下这第一次误入歧途的地方:

  • 如果customerPerson,则customer将根本不具有liveCount属性。❌

这不是真的。TypeScript中的对象类型是打开可扩展。可以在不违反原始类型的情况下向对象类型添加属性。这是一件好事,因为它使接口和类扩展能够形成类型层次结构。如果bar是类型Bar,并且Bar extends Foo,则bar也是类型Foo。但它也有一些令人讨厌的含义,就像我们在这里看到的那样。

已知类型为Person的值具有:一个名为name的属性,其类型是string;名为CCD_ 43的属性,其类型为number;以及可选地,名为address的属性,如果存在,则具有类型string | undefined。但是Person类型的值不知道缺少任何其他属性。具体而言,尚不清楚是否缺少名为liveCount的属性。而且,在这个关键点上可能存在什么样的属性是没有限制的。它可能是numberstringDate或任何东西。唯一可以肯定的是,它类似于unknownany。所以我们必须把我们的逻辑修改成这样:

  • customerCatPerson。✔
  • 如果customerCat,则customer.liveCount将是number。✔
  • 如果customerPerson,则customer可以具有liveCount属性,其类型可以是任何类型。✔
  • 那么customer.liveCount将是any。✔
  • 所以customer.liveCount将是numberany。✔
  • 因此,customer.liveCount的类型为any,首先访问customer上的liveCount属性可能是错误的。✔

示例:

const undeadCount = {
name: "Dracula",
age: 1000,
liveCount: false
}
const weirdPerson: Person = undeadCount;
deliverTo(undeadCount);

这里,undeadCount是有效的Person,因为它具有正确类型的nameage。它具有boolean类型的liveCount属性,但这并不能取消它作为Person的资格,正如我可以将它毫无错误地分配给Person类型的变量weirdPerson这一事实所证明的那样。所以我也可以打电话给deliverTo(undeadCount)。如果deliverTo()的实现期望customer.liveCountnumber,如果它不是undefined,那么可能会发生一些非常奇怪的事情。您甚至可能出现运行时错误(例如customer.liveCount?.toFixed(2))。


注意,如果你有一种说";Customer要么是精确地Cat,要么确切地Person,其中在这两种情况下都不可能存在我不知道的额外性质";。但是TypeScript目前不支持所谓的";确切类型";,并且存在在microsoft/TypeScript#12936上实现它们的长期功能请求。那么,如果没有一个合理的方式来说Exact<Cat> | Exact<Person>,该怎么办呢?


如何继续

有不同的解决方法。您可以做的一件事是断言您的customer具有可选的liveCount属性,该属性是numberundefined。当然,从技术上讲,有人可能会通过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;
};

您明确地说,如果customerPerson,那么它就缺少任何特定于Cat的属性,反之亦然。您甚至可以让编译器通过ExclusiveUnion这样的类型函数从Customer类型计算ExclusiveCustomer,这是另一个SO问题:

const { liveCount } = customer as ExclusiveUnion<Customer>;

另一种解决方法是使用缩小范围,通过检查customer的内容,将其视为PersonCat。最简单的方法是,从表面上看,与关于开放类型的答案的第一部分完全不一致:

const liveCount = "liveCount" in customer ? customer.liveCount : undefined;
// const liveCount: number | undefined

什么?是的,您可以使用in运算符作为类型保护,将customerCustomer缩小到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 CatisCat(),我们告诉编译器允许使用isCat()将某个内容缩小为Cat(如果为true)或非Cat(如果为false)。如果您呼叫isCat(customer),则表示CatPerson。我碰巧以最安全的方式实现了isCat(),但我承担了这个责任,编译器放弃了它

function isCat(x: any): x is Cat {
return Math.random() < 0.5 // 🙀
}

编译器也不会更聪明。好了。


RECAP

  • 您只能安全地访问联合的所有成员上存在的属性
  • 你在它工作的地方看到的行为是一个bug。不要指望它
  • 如果假设TypeScript中的类型是精确的,那么可能会发生奇怪的坏事
  • 但是它们可能不会发生,所以您可以使用类型断言来使编译器停止抱怨
  • 或者,您可以使用类型窄化使编译器停止抱怨,并具有不同程度的安全性

到代码的游乐场链接