Skip to content

TypeScript 联合类型

在 TypeScript 中,联合类型(Union Types)是一种特殊的类型,表示一个值可以是多种类型中的一种。联合类型使用竖线 | 来分隔不同的类型,例如 string | number 表示一个值可以是字符串或数字。本文将详细介绍 TypeScript 中的联合类型。

1. 基本用法

声明联合类型

联合类型的声明使用竖线 | 来分隔不同的类型。

typescript
// 字符串或数字类型
let value: string | number;

// 布尔值或字符串类型
let flag: boolean | string;

// 多种类型的联合
let mixed: string | number | boolean | null | undefined;

赋值

联合类型的变量可以赋值为联合类型中的任何一种类型。

typescript
let value: string | number;

value = "hello"; // 正确:字符串类型
value = 123; // 正确:数字类型

// 错误:类型不匹配
// value = true; // 类型 'boolean' 不能赋值给类型 'string | number'

函数参数

函数参数可以使用联合类型,允许传入多种类型的参数。

typescript
function printValue(value: string | number): void {
  console.log(value);
}

printValue("hello"); // 正确:字符串类型
printValue(123); // 正确:数字类型

// 错误:类型不匹配
// printValue(true); // 类型 'boolean' 不能赋值给类型 'string | number'

函数返回值

函数返回值也可以使用联合类型,表示函数可能返回多种类型的值。

typescript
function getValue(flag: boolean): string | number {
  return flag ? "hello" : 123;
}

let result: string | number = getValue(true);
console.log(result); // 输出:hello

result = getValue(false);
console.log(result); // 输出:123

2. 类型注解

基本类型联合

typescript
// 字符串或数字
let value: string | number;

// 布尔值或字符串
let flag: boolean | string;

// 数字或布尔值
let numberOrBoolean: number | boolean;

复杂类型联合

typescript
// 数组或对象
let arrayOrObject: number[] | { name: string };

// 函数或字符串
let functionOrString: (() => void) | string;

// 接口或类
interface User { id: number; name: string; }
class Person { name: string; age: number; }
let userOrPerson: User | Person;

字面量类型联合

typescript
// 字符串字面量联合
let direction: "up" | "down" | "left" | "right";
direction = "up"; // 正确
direction = "left"; // 正确

// 错误:类型不匹配
// direction = "forward"; // 类型 '"forward"' 不能赋值给类型 '"up" | "down" | "left" | "right"'

// 数字字面量联合
let dice: 1 | 2 | 3 | 4 | 5 | 6;
dice = 1; // 正确
dice = 6; // 正确

// 布尔字面量联合
let booleanLiteral: true | false;
booleanLiteral = true; // 正确
booleanLiteral = false; // 正确

嵌套联合类型

typescript
// 嵌套联合类型
let nested: (string | number) | boolean;

// 复杂嵌套
let complex: string | (number | boolean)[];

3. 类型守卫

在 TypeScript 中,当使用联合类型时,需要使用类型守卫来确定具体的类型,以便进行相应的操作。

typeof 类型守卫

使用 typeof 操作符来检查值的类型。

typescript
function processValue(value: string | number): void {
  if (typeof value === "string") {
    // 在这个分支中,TypeScript 知道 value 是 string 类型
    console.log(value.toUpperCase());
  } else {
    // 在这个分支中,TypeScript 知道 value 是 number 类型
    console.log(value.toFixed(2));
  }
}

processValue("hello"); // 输出:HELLO
processValue(123.45); // 输出:123.45

instanceof 类型守卫

使用 instanceof 操作符来检查对象的类型。

typescript
class Dog {
  bark() { console.log("Woof!"); }
}

class Cat {
  meow() { console.log("Meow!"); }
}

function processPet(pet: Dog | Cat): void {
  if (pet instanceof Dog) {
    // 在这个分支中,TypeScript 知道 pet 是 Dog 类型
    pet.bark();
  } else {
    // 在这个分支中,TypeScript 知道 pet 是 Cat 类型
    pet.meow();
  }
}

processPet(new Dog()); // 输出:Woof!
processPet(new Cat()); // 输出:Meow!

字面量类型守卫

使用字面量类型来检查值的具体值。

typescript
type Direction = "up" | "down" | "left" | "right";

function processDirection(direction: Direction): void {
  switch (direction) {
    case "up":
      console.log("Moving up");
      break;
    case "down":
      console.log("Moving down");
      break;
    case "left":
      console.log("Moving left");
      break;
    case "right":
      console.log("Moving right");
      break;
  }
}

processDirection("up"); // 输出:Moving up
processDirection("left"); // 输出:Moving left

in 操作符类型守卫

使用 in 操作符来检查对象是否具有某个属性。

typescript
interface Bird {
  fly(): void;
  layEggs(): void;
}

interface Fish {
  swim(): void;
  layEggs(): void;
}

function processAnimal(animal: Bird | Fish): void {
  if ("fly" in animal) {
    // 在这个分支中,TypeScript 知道 animal 是 Bird 类型
    animal.fly();
  } else {
    // 在这个分支中,TypeScript 知道 animal 是 Fish 类型
    animal.swim();
  }
  // 两者都有 layEggs 方法
  animal.layEggs();
}

const bird: Bird = {
  fly() { console.log("Flying"); },
  layEggs() { console.log("Laying bird eggs"); }
};

const fish: Fish = {
  swim() { console.log("Swimming"); },
  layEggs() { console.log("Laying fish eggs"); }
};

processAnimal(bird); // 输出:Flying  Laying bird eggs
processAnimal(fish); // 输出:Swimming  Laying fish eggs

自定义类型守卫

使用自定义函数来检查类型。

typescript
interface User {
  id: number;
  name: string;
}

interface Admin {
  id: number;
  name: string;
  role: string;
}

function isAdmin(user: User | Admin): user is Admin {
  return "role" in user;
}

function processUser(user: User | Admin): void {
  if (isAdmin(user)) {
    // 在这个分支中,TypeScript 知道 user 是 Admin 类型
    console.log(`Admin ${user.name} with role ${user.role}`);
  } else {
    // 在这个分支中,TypeScript 知道 user 是 User 类型
    console.log(`User ${user.name}`);
  }
}

const user: User = { id: 1, name: "John" };
const admin: Admin = { id: 2, name: "Jane", role: "admin" };

processUser(user); // 输出:User John
processUser(admin); // 输出:Admin Jane with role admin

4. 联合类型的操作

访问公共属性

当使用联合类型时,只能访问所有类型共有的属性和方法。

typescript
interface Bird {
  fly(): void;
  layEggs(): void;
  name: string;
}

interface Fish {
  swim(): void;
  layEggs(): void;
  name: string;
}

function processAnimal(animal: Bird | Fish): void {
  // 可以访问公共属性 name
  console.log(animal.name);
  
  // 可以调用公共方法 layEggs
  animal.layEggs();
  
  // 错误:不能访问 Bird 特有的方法 fly
  // animal.fly();
  
  // 错误:不能访问 Fish 特有的方法 swim
  // animal.swim();
}

类型断言

使用类型断言来指定具体的类型。

typescript
function processValue(value: string | number): void {
  // 使用类型断言
  const strValue = value as string;
  console.log(strValue.length); // 可能会出错,如果 value 是 number 类型
  
  // 推荐:先使用类型守卫
  if (typeof value === "string") {
    console.log(value.length); // 安全,TypeScript 知道 value 是 string 类型
  }
}

processValue("hello"); // 输出:5
// processValue(123); // 运行时错误:value.length is not a function

类型缩小

TypeScript 会根据上下文自动缩小联合类型的范围。

typescript
function processValue(value: string | number | null): void {
  if (value === null) {
    console.log("Value is null");
    return;
  }
  
  // 在这里,TypeScript 知道 value 不是 null,类型缩小为 string | number
  if (typeof value === "string") {
    console.log(value.length); // 安全,TypeScript 知道 value 是 string 类型
  } else {
    console.log(value.toFixed(2)); // 安全,TypeScript 知道 value 是 number 类型
  }
}

processValue("hello"); // 输出:5
processValue(123.45); // 输出:123.45
processValue(null); // 输出:Value is null

5. 联合类型的最佳实践

1. 使用类型守卫

当使用联合类型时,总是使用类型守卫来确定具体的类型,以便进行相应的操作。

typescript
// 推荐:使用类型守卫
function processValue(value: string | number): void {
  if (typeof value === "string") {
    console.log(value.toUpperCase());
  } else {
    console.log(value.toFixed(2));
  }
}

// 不推荐:使用类型断言
function processValue(value: string | number): void {
  console.log((value as string).toUpperCase()); // 可能会出错
}

2. 合理使用联合类型

只在必要时使用联合类型,避免过度使用导致代码复杂。

typescript
// 推荐:合理使用联合类型
function printId(id: string | number): void {
  console.log(`ID: ${id}`);
}

// 不推荐:过度使用联合类型
function processValue(value: string | number | boolean | object | null | undefined): void {
  // 代码会变得复杂
}

3. 使用字面量类型联合

对于有限的可能值,使用字面量类型联合可以提供更精确的类型检查。

typescript
// 推荐:使用字面量类型联合
type Direction = "up" | "down" | "left" | "right";
function move(direction: Direction): void {
  console.log(`Moving ${direction}`);
}

// 不推荐:使用字符串类型
function move(direction: string): void {
  console.log(`Moving ${direction}`);
}

4. 使用类型别名

对于复杂的联合类型,使用类型别名可以提高代码的可读性。

typescript
// 推荐:使用类型别名
type StringOrNumber = string | number;
function processValue(value: StringOrNumber): void {
  // 代码
}

// 不推荐:直接使用复杂联合类型
function processValue(value: string | number | boolean | null | undefined): void {
  // 代码
}

5. 注意联合类型的顺序

在使用联合类型时,注意类型的顺序,将更具体的类型放在前面。

typescript
// 推荐:将更具体的类型放在前面
type Animal = Dog | Cat | Animal;

// 不推荐:将更一般的类型放在前面
type Animal = Animal | Dog | Cat; // 永远不会匹配到 Dog 和 Cat

6. 常见错误

1. 访问非公共属性

当使用联合类型时,只能访问所有类型共有的属性和方法。

typescript
interface Bird {
  fly(): void;
  name: string;
}

interface Fish {
  swim(): void;
  name: string;
}

function processAnimal(animal: Bird | Fish): void {
  // 错误:不能访问 Bird 特有的方法 fly
  // animal.fly();
  
  // 错误:不能访问 Fish 特有的方法 swim
  // animal.swim();
  
  // 正确:可以访问公共属性 name
  console.log(animal.name);
}

2. 类型断言错误

使用类型断言时,如果类型不匹配,会导致运行时错误。

typescript
function processValue(value: string | number): void {
  // 错误:如果 value 是 number 类型,会导致运行时错误
  const strValue = value as string;
  console.log(strValue.length);
}

processValue(123); // 运行时错误:value.length is not a function

3. 忘记使用类型守卫

当使用联合类型时,忘记使用类型守卫会导致类型错误。

typescript
function processValue(value: string | number): void {
  // 错误:TypeScript 不知道 value 是 string 类型
  // console.log(value.length);
  
  // 正确:使用类型守卫
  if (typeof value === "string") {
    console.log(value.length);
  }
}

4. 联合类型与 null/undefined

当联合类型包含 null 或 undefined 时,需要处理这些情况。

typescript
function processValue(value: string | null): void {
  // 错误:可能为 null
  // console.log(value.length);
  
  // 正确:处理 null 情况
  if (value !== null) {
    console.log(value.length);
  }
}

processValue("hello"); // 输出:5
processValue(null); // 无输出

5. 复杂联合类型的性能

过度使用复杂的联合类型可能会影响 TypeScript 的编译性能。

typescript
// 注意:过于复杂的联合类型可能会影响编译性能
type ComplexType = string | number | boolean | object | null | undefined | Array<string> | Array<number> | { name: string } | { age: number };

7. 联合类型与其他类型的关系

联合类型与交叉类型

联合类型表示 "或" 的关系,而交叉类型表示 "与" 的关系。

typescript
// 联合类型:string 或 number
let union: string | number;

// 交叉类型:同时具有 A 和 B 的属性
interface A { a: number; }
interface B { b: string; }
let intersection: A & B = { a: 1, b: "hello" };

联合类型与字面量类型

字面量类型是一种特殊的类型,表示具体的字面量值。

typescript
// 字符串字面量联合
let direction: "up" | "down" | "left" | "right";

// 数字字面量联合
let dice: 1 | 2 | 3 | 4 | 5 | 6;

// 布尔字面量联合
let booleanLiteral: true | false;

联合类型与类型推断

TypeScript 会根据上下文自动推断联合类型。

typescript
// 类型推断为 string | number
let value = Math.random() > 0.5 ? "hello" : 123;

// 类型推断为 "a" | "b"
let str = Math.random() > 0.5 ? "a" : "b";

8. 实际应用场景

1. 处理不同类型的输入

typescript
function formatInput(input: string | number | boolean): string {
  if (typeof input === "string") {
    return input.toUpperCase();
  } else if (typeof input === "number") {
    return input.toFixed(2);
  } else {
    return input ? "true" : "false";
  }
}

console.log(formatInput("hello")); // 输出:HELLO
console.log(formatInput(123.456)); // 输出:123.46
console.log(formatInput(true)); // 输出:true

2. 可选参数和默认值

typescript
function greet(name: string, age?: number | undefined): string {
  if (age) {
    return `Hello, ${name}! You are ${age} years old.`;
  } else {
    return `Hello, ${name}!`;
  }
}

console.log(greet("John")); // 输出:Hello, John!
console.log(greet("John", 30)); // 输出:Hello, John! You are 30 years old.

3. 处理不同类型的返回值

typescript
function getValue(flag: boolean): string | number {
  return flag ? "hello" : 123;
}

function processResult(result: string | number): void {
  if (typeof result === "string") {
    console.log(`String result: ${result}`);
  } else {
    console.log(`Number result: ${result}`);
  }
}

const result1 = getValue(true);
processResult(result1); // 输出:String result: hello

const result2 = getValue(false);
processResult(result2); // 输出:Number result: 123

4. 字面量类型联合的应用

typescript
type HttpMethod = "GET" | "POST" | "PUT" | "DELETE";

function sendRequest(url: string, method: HttpMethod): void {
  console.log(`Sending ${method} request to ${url}`);
}

sendRequest("https://api.example.com/users", "GET"); // 正确
sendRequest("https://api.example.com/users", "POST"); // 正确

// 错误:类型不匹配
// sendRequest("https://api.example.com/users", "PATCH"); // 类型 '"PATCH"' 不能赋值给类型 'HttpMethod'

5. 处理可选属性

typescript
interface User {
  id: number;
  name: string;
  email?: string;
  age?: number;
}

function createUser(user: User): void {
  console.log(`Creating user: ${user.name}`);
  if (user.email) {
    console.log(`Email: ${user.email}`);
  }
  if (user.age) {
    console.log(`Age: ${user.age}`);
  }
}

createUser({ id: 1, name: "John" });
// 输出:
// Creating user: John

createUser({ id: 2, name: "Jane", email: "jane@example.com", age: 30 });
// 输出:
// Creating user: Jane
// Email: jane@example.com
// Age: 30

总结

TypeScript 中的联合类型是一种强大的类型特性,它允许一个值可以是多种类型中的一种。它包括:

  • 基本用法:声明联合类型,赋值,函数参数和返回值
  • 类型注解:基本类型联合,复杂类型联合,字面量类型联合,嵌套联合类型
  • 类型守卫:typeof 类型守卫,instanceof 类型守卫,字面量类型守卫,in 操作符类型守卫,自定义类型守卫
  • 联合类型的操作:访问公共属性,类型断言,类型缩小
  • 最佳实践:使用类型守卫,合理使用联合类型,使用字面量类型联合,使用类型别名,注意联合类型的顺序
  • 常见错误:访问非公共属性,类型断言错误,忘记使用类型守卫,联合类型与 null/undefined,复杂联合类型的性能
  • 联合类型与其他类型的关系:联合类型与交叉类型,联合类型与字面量类型,联合类型与类型推断
  • 实际应用场景:处理不同类型的输入,可选参数和默认值,处理不同类型的返回值,字面量类型联合的应用,处理可选属性

通过合理使用联合类型,你可以在 TypeScript 中更灵活地处理多种类型的情况,提高代码的可读性和类型安全性。