同事看完这几道题,发现 TS 交叉类型竟还没入门!

// 非对象类型交叉运算
type N0 = string & number;
type N1 = any & 1;
type N2 = any & never;

// 对象类型交叉运算
type A = { kind: 'a', foo: string };
type B = { kind: 'b', foo: number };
type C = { kind: 'c', foo: number };

type AB = A & B;
type BC = B & C;

// 函数类型交叉运算
type F1 = (a: string, b: string) => void;
type F2 = (a: number, b: number) => void;

type Fn = F1 & F2

在学习 TypeScript 的过程中,你可以把类型理解成一系列值的集合。比如,你可以把数字类型看作是所有数字的集合,1.0、68 就属于这个集合中,而 "阿宝哥" 就不属于这个集合,因为它属于字符串类型。

新源网站制作公司哪家好,找创新互联!从网页设计、网站建设、微信开发、APP开发、响应式网站等网站项目制作,到程序开发,运营维护。创新互联自2013年创立以来到现在10年的时间,我们拥有了丰富的建站经验和运维经验,来保证我们的工作的顺利进行。专注于网站建设就选创新互联

同样,对于对象类型来说,我们也可以把它理解成对象的集合。比如以上代码中 Point 类型表示含有 x 和 y 属性,且属性值的类型都是 number 类型对象的集合。而 Named 类型表示含有 name 属性且属性值的类型是 string 类型对象的集合。

interface Point {
x: number;
y: number;
}

interface Named {
name: string;
}

在集合论中,假设 A,B 是两个集合,由所有属于集合 A 且属于集合 B 的元素所组成的集合,叫做集合 A 与集合 B 的交集。

当我们对 Point 类型和 Named 类型进行交集运算,就会产生新的类型。该类型中所包含的对象既属于 Point 类型,又属于 Named 类型。

在 TypeScript 中为我们提供了交叉运算符,来实现对多种类型进行交叉运算,所产生的新类型也被称为交叉类型。

下面我们来简单介绍一下交叉运算符,该运算符满足以下这些特性:

  • 唯一性:A & A 等价于 A
  • 满足交换律:A & B 等价于 B & A
  • 满足结合律:(A & B) & C 等价于 A & (B & C)
  • 父类型收敛:如果 B 是 A 的父类型,则 A & B 将被收敛成 A 类型
type A0 = 1 & number; // 1
type A1 = "1" & string; // "1"
type A2 = true & boolean; // true

type A3 = any & 1; // any
type A4 = any & boolean; // any
type A5 = any & never; // never

在以上代码中,any 类型和 never 类型比较特殊。除了 never 类型之外,任何类型与 any 类型进行交叉运算的结果都是 any 类型。

介绍完交叉运算符之后,我们来看一下对 Point 类型和 Named 类型进行交叉运算后,将产生什么样的类型?

interface Point {
x: number;
y: number;
}

interface Named {
name: string;
}

type NamedPoint = Point & Named
// {
//. x: number;
//. y: number;
//. name: string;
//. }

在以上代码中,新产生的 NamedPoint 类型将会同时包含 x、y 和 name 属性。但如果进行交叉运算的多个对象类型中,包含相同的属性但属性的类型不一致结果又会是怎样呢?

interface X {
c: string;
d: string;
}

interface Y {
c: number;
e: string
}

type XY = X & Y;
type YX = Y & X;

在以上代码中,接口 X 和接口 Y 都含有一个相同的 c 属性,但它们的类型不一致。对于这种情况,此时 XY 类型或 YX 类型中 c 属性的类型是不是可以是 string 或 number 类型呢?下面我们来验证一下:

let p: XY = { c: "c", d: "d", e: "e" }; // Error
let q: YX = { c: 6, d: "d", e: "e" }; // Error

为什么接口 X 和接口 Y 进行交叉运算后,c 属性的类型会变成 never 呢?这是因为运算后 c 属性的类型为 string & number,即 c 属性的类型既可以是 string 类型又可以是 number 类型。很明显这种类型是不存在的,所以运算后 c 属性的类型为 never 类型。

在前面示例中,刚好接口 X 和接口 Y 中 c 属性的类型都是基本数据类型。那么如果不同的对象类型中含有相同的属性,且属性类型是非基本数据类型的话,结果又会是怎样呢?我们来看个具体的例子:

interface D { d: boolean; }
interface E { e: string; }
interface F { f: number; }

interface A { x: D; }
interface B { x: E; }
interface C { x: F; }

type ABC = A & B & C;

let abc: ABC = { // Ok
x: {
d: true,
e: '阿宝哥',
f: 666
}
};

由以上结果可知,在对多个类型进行交叉运算时,若存在相同的属性且属性类型是对象类型,那么属性会按照对应的规则进行合并。

但需要注意的是,在对对象类型进行交叉运算的时候,如果对象中相同的属性被认为是可辨识的属性,即属性的类型是字面量类型或字面量类型组成的联合类型,那么最终的运算结果将是 never 类型:

type A = { kind: 'a', foo: string };
type B = { kind: 'b', foo: number };
type C = { kind: 'c', foo: number };

type AB = A & B; // never
type BC = B & C; // never

在以上代码中,A、B、C 三种对象类型都含有 kind 属性且属性的类型都是字符串字面量类型,所以 AB 类型和 BC 类型最终都是 never 类型。接下来,我们来继续看个例子:

type Foo = {
name: string,
age: number
}

type Bar = {
name: number,
age: number
}

type Baz = Foo & Bar
// {
//. name: never;
//. age: number;
// }

在以上代码中,Baz 类型是含有 name 属性和 age 属性的对象类型,其中 name 属性的类型是 never 类型,而 age 属性的类型是 number 类型。

但如果把 Foo 类型中 name 属性的类型改成 boolean 类型的话,Baz 类型将会变成 never 类型。这是因为 boolean 类型可以理解成由 true 和 false 字面量类型组成的联合类型。

type Foo = {
name: boolean, // true | false
age: number
}

type Bar = {
name: number,
age: number
}

type Baz = Foo & Bar // never

其实除了对象类型可以进行交叉运算外,函数类型也可以进行交叉运算:

type F1 = (a: string, b: string) => void;  
type F2 = (a: number, b: number) => void;

let f: F1 & F2 = (a: string | number, b: string | number) => { };
f("hello", "world"); // Ok
f(1, 2); // Ok
f(1, "test"); // Error

对于以上代码中的函数调用语句,只有 f(1, "test") 的调用语句会出现错误,其对应的错误信息如下:

没有与此调用匹配的重载。
第 1 个重载(共 2 个),“(a: string, b: string): void”,出现以下错误。
类型“number”的参数不能赋给类型“string”的参数。
第 2 个重载(共 2 个),“(a: number, b: number): void”,出现以下错误。
类型“string”的参数不能赋给类型“number”的参数。ts(2769)

根据以上的错误信息,我们可以了解到 TypeScript 编译器会利用函数重载的特性来实现不同函数类型的交叉运算,要解决上述问题,我们可以在定义一个新的函数类型 F3,具体如下:

type F1 = (a: string, b: string) => void;  
type F2 = (a: number, b: number) => void;
type F3 = (a: number, b: string) => void;

let f: F1 & F2 & F3 = (a: string | number, b: string | number) => { };
f("hello", "world"); // Ok
f(1, 2); // Ok
f(1, "test"); // Ok

掌握了交叉类型之后,在结合往期文章中介绍的映射类型,我们就可以根据工作需要实现一些自定义工具类型了。比如实现一个 PartialByKeys 工具类型,用于把对象类型中指定的 keys 变成可选的。

type User = {
id: number;
name: string;
age: number;
}

type PartialByKeys = Simplify<{
[P in K]?: T[P]
} & Pick>>

type U1 = PartialByKeys
type U2 = PartialByKeys

那么如果让你实现一个 RequiredByKeys 工具类型,用于把对象类型中指定的 keys 变成必填的,你知道怎么实现么?知道答案的话,你喜欢以这种形式学 TS 么?

当前名称:同事看完这几道题,发现 TS 交叉类型竟还没入门!
本文路径:http://www.gawzjz.com/qtweb2/news26/5126.html

网站建设、网络推广公司-创新互联,是专注品牌与效果的网站制作,网络营销seo公司;服务项目有等

广告

声明:本网站发布的内容(图片、视频和文字)以用户投稿、用户转载内容为主,如果涉及侵权请尽快告知,我们将会在第一时间删除。文章观点不代表本网站立场,如需处理请联系客服。电话:028-86922220;邮箱:631063699@qq.com。内容未经允许不得转载,或转载时需注明来源: 创新互联