TypeScript是构建可扩展JavaScript应用的强大工具,几乎已成为大型Web JavaScript项目的标准。然而,对于新手来说,TypeScript也存在一些棘手之处,其中之一就是区分联合类型。
考虑以下代码:
interface Cat { weight: number; whiskers: number; } interface Dog { weight: number; friendly: boolean; } let animal: Dog | Cat;
许多开发者会惊讶地发现,访问animal
时,只有weight
属性是有效的,whiskers
和friendly
属性无效。本文将解释其中的原因。
在深入探讨之前,让我们快速回顾一下结构类型和名义类型,这将有助于理解TypeScript的区分联合类型。
理解结构类型最好的方法是将其与名义类型进行对比。大多数你可能用过的类型化语言都是名义类型化的。例如C#代码(Java或C 类似):
class Foo { public int x; } class Blah { public int x; }
即使Foo
和Blah
的结构完全相同,它们也不能相互赋值。以下代码:
Blah b = new Foo();
会产生以下错误:
<code>无法隐式转换类型“Foo”为“Blah”</code>
这些类的结构无关紧要。Foo
类型的变量只能赋值给Foo
类的实例(或其子类)。
TypeScript则相反,它采用结构类型。如果两个类型具有相同的结构,TypeScript就认为它们是兼容的。
因此,以下代码可以正常运行:
class Foo { x: number = 0; } class Blah { x: number = 0; } let f: Foo = new Blah(); let b: Blah = new Foo();
让我们进一步说明这一点。给定以下代码:
class Foo { x: number = 0; } let f: Foo;
f
是一个变量,它可以保存任何与Foo
类实例结构相同的对象,在本例中,这意味着一个表示数字的x
属性。这意味着即使是一个普通的JavaScript对象也可以被接受。
let f: Foo; f = { x: 0 };
让我们回到本文开头的代码:
interface Cat { weight: number; whiskers: number; } interface Dog { weight: number; friendly: boolean; }
我们知道:
let animal: Dog;
使animal
成为任何与Dog
接口结构相同的对象。那么以下代码是什么意思?
let animal: Dog | Cat;
这将animal
类型定义为任何与Dog
接口匹配的对象,或者任何与Cat
接口匹配的对象。
那么为什么现在的animal
只允许我们访问weight
属性呢?简单来说,是因为TypeScript不知道它是什么类型。TypeScript知道animal
必须是Dog
或Cat
,但它可能是两者中的任何一个。如果允许我们访问friendly
属性,但在实例中animal
实际上是Cat
而不是Dog
,则可能会导致运行时错误。对于whiskers
属性也是如此。
联合类型是有效值的联合,而不是属性的联合。开发者经常写这样的代码:
let animal: Dog | Cat;
并期望animal
拥有Dog
和Cat
属性的联合。但这又是一个错误。这指定animal
具有一个值,该值与有效的Dog
值和有效的Cat
值的联合匹配。但是TypeScript只允许你访问它知道存在的属性。目前,这意味着联合中所有类型的属性。
现在,我们有:
let animal: Dog | Cat;
当animal
是Dog
时,我们如何正确地将其视为Dog
并访问Dog
接口上的属性,当它是Cat
时也同样处理?目前,我们可以使用in
运算符。这是一个你可能不太常看到的旧式JavaScript运算符,它允许我们测试一个属性是否存在于一个对象中。例如:
let o = { a: 12 }; "a" in o; // true "x" in o; // false
事实证明,TypeScript与in
运算符深度集成。让我们看看如何使用:
let animal: Dog | Cat = {} as any; if ("friendly" in animal) { console.log(animal.friendly); } else { console.log(animal.whiskers); }
这段代码不会产生错误。在if
块内,TypeScript知道存在friendly
属性,因此将animal
转换为Dog
。在else
块内,TypeScript类似地将animal
视为Cat
。你甚至可以在代码编辑器中将鼠标悬停在这些块中的animal
对象上查看这一点。
你可能期望博文到此结束,但不幸的是,通过检查属性是否存在来收窄联合类型非常有限。它对我们简单的Dog
和Cat
类型很有效,但是当我们有更多类型以及这些类型之间更多重叠时,事情很容易变得更复杂,也更脆弱。
这就是区分联合类型派上用场的地方。我们将保留之前的所有内容,只是向每种类型添加一个属性,其唯一作用是区分(或“区分”)这些类型:
interface Cat { weight: number; whiskers: number; ANIMAL_TYPE: "CAT"; } interface Dog { weight: number; friendly: boolean; ANIMAL_TYPE: "DOG"; }
请注意两种类型上的ANIMAL_TYPE
属性。不要将其误认为是具有两个不同值的字符串;这是一个字面量类型。ANIMAL_TYPE: "CAT";
表示一个恰好包含字符串“CAT”的类型,仅此而已。
现在我们的检查变得更可靠:
let animal: Dog | Cat = {} as any; if (animal.ANIMAL_TYPE === "DOG") { console.log(animal.friendly); } else { console.log(animal.whiskers); }
假设联合中参与的每个类型都具有ANIMAL_TYPE
属性的不同值,则此检查将万无一失。
唯一的缺点是你现在需要处理一个新属性。每次创建Dog
或Cat
的实例时,都必须为ANIMAL_TYPE
提供唯一正确的数值。但不用担心忘记,因为TypeScript会提醒你。?
如果你想了解更多信息,我建议阅读TypeScript文档关于类型收窄的部分。这将更深入地介绍我们这里讨论的内容。在该链接中有一个关于类型断言的部分。这些允许你定义你自己的自定义检查来收窄类型,而无需使用类型鉴别器,也无需依赖in
关键字。
在本文开头,我说过,在以下示例中,为什么weight
是唯一可访问的属性:
interface Cat { weight: number; whiskers: number; } interface Dog { weight: number; friendly: boolean; } let animal: Dog | Cat;
我们学到的是,TypeScript只知道animal
可以是Dog
或Cat
,但不能同时是两者。因此,我们只能得到weight
,它是两者之间唯一的公共属性。
区分联合类型的概念是TypeScript区分这些对象的方式,并且这种方式非常具有可扩展性,即使对于更大的对象集合也是如此。因此,我们必须在两种类型上创建一个新的ANIMAL_TYPE
属性,该属性保存我们可以用来检查的单个字面量值。当然,这是另一件需要跟踪的事情,但它也产生了更可靠的结果——这正是我们首先从TypeScript想要得到的。
以上是揭开打字稿歧视工会的神秘面纱的详细内容。更多信息请关注PHP中文网其他相关文章!