TypeScript 联合类型

在前面的学习中,我们给变量定义类型时,使用的都是非常明确的 “单一类型”。比如定义为 number 就只能存数字,定义为 string 就只能存字符串。

但在实际的业务开发中,数据往往会更加的灵活。比如我们有一个用于打印用户 id 的函数,后端的同事告诉你:“这个 id 有时候是数字(比如 1001),有时候是字符串(比如 "A-1001")。”

此时,如果我们把参数定死为 number,那么传入字符串则会报错。如果定死为 string,那么传数字又会报错。鸡贼的你可能会灵机一动:“那我用 any 呗!”

您可千万不要这样做!一旦用了 any,TypeScript 的类型保护就彻底失效了。面对这种 “既可能是 A、又可能是 B” 的场景,TypeScript 为我们提供了一个完美的解决方案——联合类型(Union Types)

TypeScript 联合类型是什么?

在 TypeScript 中,我们可以使用联合类型来表示一个值可以是几种特定类型中的某一种。

语法:

const 变量名: 类型1 | 类型2 | 类型3 = 值;

说明:

其中,不同类型之间使用 “竖线(|)”来分隔开,类似于逻辑运算中的 “或”。

示例 1:定义联合类型

function printId(id: number | string) {
    console.log("当前用户 id 是:", id);
}

// 传入数字
const userId1: number = 1001;
printId(userId1);

// 输入字符串
const userId2: string = "A-1001";
printId(userId2);

运行结果如下。

当前用户 id 是:1001
当前用户 id 是:A-1001

分析:

这里我们定义一个用于处理 id 的函数,参数既可以是数字,也可以是字符串。对于这个例子来说,如果我们传入一个布尔值,则会直接拦截报错:

// 传入布尔值
const userId3: boolean = true;
printId(userId3);
// 报错:类型 “boolean” 的参数不能赋给类型 “string | number” 的参数。

TypeScript 联合类型的限制

联合类型虽然好用,但在使用它的属性或方法时,TypeScript 会非常严格:它只允许你访问联合类型中所有类型共有的属性和方法。

示例 2:联合类型的限制

function getLength(value: string | number) {
    console.log(value.length); 
    // 报错:类型 “number”上不存在属性 “length”。
}

分析:

由于 value 有可能是 number,而数字是没有 length 属性的。因此为了绝对的安全,TypeScript 直接禁止了这种危险操作。

既然报错了,那我们应该如何安全地操作它呢?此时就需要使用在前面 “TypeScript any 与 unknown” 这一节中学过的 “类型收窄” 了。

示例 3:使用类型收窄

function printLength(value: string | number) {
    // 使用类型收窄
    if (typeof value === "string") {
        // 在这里,TypeScript 确信 value 是字符串,可以安全调用 length
        console.log("这是个字符串,长度为:", value.length);
    } else {
        // 在这里,TypeScript 确信 value 是数字
        console.log("这是个数字,大小为:", value);
    }
}

// 字符串
const val1: string = "绿叶网";
printLength(val1);

// 数字
const val2: number = 666;
printLength(val2);

运行结果如下。

这是个字符串,长度为: 3
这是个数字,大小为: 666

TypeScript 联合类型与数组

在 TypeScript 中,如果要定义一个数组,里面既有数字又有字符串,又应该怎么写呢?这就需要将联合类型和数组结合起来。

示例 4:包含多种类型的数组

const arr: (number | string)[] = [1, "Jack", 2, "Lucy"];

console.log(arr[1]);

运行结果如下。

Jack

分析:

这里小伙伴们要特别注意:我们必须要使用小括号 “()” 把联合类型括起来。

如果我们写成 const arr: number | string[],而没有使用小括号包裹起来,那么TypeScript 会将它理解为 “这个变量要么是一个纯数字,要么是一个纯字符串数组”,这样就和我们想要的 “既包含数字又包含字符串的数组” 完全是两个意思了。

// 正确
const arr: (number | string)[] = [1, "Jack", 2, "Lucy"];

// 错误
const arr: number | string[] = [1, "Jack", 2, "Lucy"];

TypeScript 联合类型与接口

在前面几个例子中,我们联合的都是基础数据类型(如数字、字符串)。但在实际的企业级开发中,我们更常遇到的是对 “接口(interface)” 的联合。

比如在电商后台,一个处理人员信息的函数,它接收的参数既可能是一个 “买家(Buyer)”,也可能是一个 “卖家(Seller)”。

示例 5:接口的联合类型

// 定义买家接口
interface Buyer {
    name: string;
    level: number;
}

// 定义卖家接口
interface Seller {
    name: string;
    shopName: string;
}

// 打印人员信息:参数可能是买家,也可能是卖家
function printPersonInfo(person: Buyer | Seller) {
    // 只有两者共有的属性,才能被直接安全访问!
    console.log("姓名:", person.name);
    
    // 错误操作:不能直接访问非共有属性
    // console.log(person.shopName);
    // 报错:类型 “Buyer | Seller” 上不存在属性 “shopName”。
}

const currentBuyer: Buyer = {
    name: "Jack",
    level: 3
};

printPersonInfo(currentBuyer);

运行结果如下。

姓名:Jack

分析:

当我们将两个接口进行联合时,TypeScript 依然会严格遵守这样一种安全法则:“只能访问它们共有的属性”

在这个例子中,Buyer 和 Seller 都有 name 属性,所以访问 person.name 是绝对安全的。但如果想要访问 shopName,那么 TypeScript 就会报错拦截,因为它害怕你传进来的是一个买家(Buyer),而买家(Buyer)是没有店铺名称的。

企业级最佳实践 —— 打标签

既然只能访问共有属性,那在真实项目中我们该如何区分传进来的到底是买家(Buyer)还是卖家(Seller)呢?

在企业级开发中,为了后续能够优雅地解开这种 “接口联合”,我们通常会在定义接口时,故意给它们加上一个取值固定的共有属性作为 “身份标签”。比如:

interface Buyer {
    role: "buyer"; // 身份标签
    name: string;
    level: number;
}

interface Seller {
    role: "seller"; // 身份标签
    name: string;
    shopName: string;
}

这种带有 “标签” 的联合类型,也被称为 “可辨识联合(Discriminated Unions)”。

至于如何利用这个 role 标签安全地访问后续那些 “非共有” 的属性,我们将在后续的 “TypeScript 类型保护” 一节中会详细介绍。

TypeScript 联合类型的应用

TypeScript 联合类型最强大的地方,往往是配合 “字面量类型” 和上一节学过的 “类型别名” 一起使用。这在 Vue 或 React 的大型开发中,经常用于限制组件的 Props 取值范围。

示例 6:限制固定取值范围

// 定义联合类型:限制尺寸只能是这 3 个字符串之一
type ButtonSize = "small" | "medium" | "large";

// 正常赋值
const currentSize: ButtonSize = "medium";
console.log("当前按钮尺寸:", currentSize);

// 错误赋值:
// const wrongSize: ButtonSize = "mini"; 
// 报错:不能将类型 "mini" 分配给类型 “ButtonSize”。

运行结果如下。

当前按钮尺寸:medium

分析:

这种方式比单纯使用 string 类型要安全百倍,因为它直接在编写阶段锁死了数据的取值可能性。

而且,当我们在代码里敲下 “const size: ButtonSize =” 的时候,编辑器会自动弹出 "small"、"medium"、"large" 供我们选择(如下图所示),开发体验直接拉满。

VS Code 给出智能提示

在某些轻量级的固定集合场景下,这种写法甚至比使用枚举(Enum)还要方便干净。

给站长反馈

绿叶网正在不断完善中,小伙伴们如果发现任何问题,还望多多给站长反馈,谢谢!

邮箱:lvyenet@vip.qq.com

「绿叶网」服务号
绿叶网服务号放大
关注服务号,微信也能看教程。
绿叶网服务号