返回 导航

其他

hangge.com

TypeScript - 数据类型详解3(对象类型、联合类型、类型别名)

作者:hangge | 2026-08-01 13:36
    在前面的文章中,我们介绍了 TypeScript 的基础数据类型(如 stringnumberboolean 等),以及数组类型和元组类型。本文将继续深入探讨 TypeScript 的类型系统,重点讲解 对象类型联合类型类型别名 这三种高级类型:对象类型 用于描述具有多个属性的数据结构,联合类型 用于处理可能存在多种类型的情况,类型别名 则为复杂类型提供了简洁的命名方式。下面我们将逐一详细介绍它们的用法。

一、对象类型

1,基本概念

(1)在 JavaScript 中,我们分组和传递数据的基本方式是通过对象。在 TypeScript 中,我们通过 对象类型 来表示它们。对象类型用于描述具有多个属性的 JavaScript 值,几乎是最常用的类型之一。

(2)定义对象类型的方式很简单,只需列出其属性及其类型。例如,我们希望限定一个函数接收的参数为具有 xy 属性的对象类型,我们可以使用 { x: number; y: number } 作为参数的类型。这个类型描述了一个包含两个属性的对象:x 属性的类型是 numbery 属性的类型也是 number
提示:对象类型中的属性之间可以使用 (逗号)或 ;(分号)分隔,最后一个分隔符是可选的。每个属性的类型也是可选的,如果不指定类型,默认为 any 类型。但为了类型安全,建议始终为每个属性指定明确的类型。
// 为参数 point 指定对象类型: {x: number, y: number}
function printPoint(point: { x: number; y: number }) {
  console.log(point.x);
  console.log(point.y);
}

// 调用函数,传入符合类型的对象
printPoint({ x: 200, y: 100 });

2,可选属性

(1)在实际开发中,某些属性可能并不总是必须存在。比如一个坐标点可能有 z 坐标(三维坐标),也可能没有(二维坐标)。在对象类型中,我们可以通过在属性名后面添加问号 ? 的方式来指定某些属性为 可选属性

(2)下面是一个使用可选属性的示例:
提示:可选属性的主要好处是:一方面可以对可能存在的属性进行预定义,另一方面可以捕获引用不存在的属性时的错误。但在使用可选属性时,需要注意先判断其是否存在再进行操作。
// 为参数 point 指定对象类型,其中 z 属性为可选属性
function printPoint(point: { x: number; y: number; z?: number }) {
  console.log(point.x);
  console.log(point.y);
  
  // 可选属性可能不存在,需要判断后再使用
  if (point.z !== undefined) {
    console.log(point.z);
  }
}

// 不传递 z 属性也可以,因为 z 属性是可选的
printPoint({ x: 123, y: 321 });

// 传递 z 属性同样合法
printPoint({ x: 123, y: 321, z: 111 });

3,只读属性

(1)在某些场景下,我们希望对象的某些属性在创建后就不能被修改。比如用户对象的 id 属性,一旦创建就不应该被改变。这时可以使用 readonly 修饰符来标记属性为 只读属性

(2)下面是只读属性的使用示例:
提示readonly 修饰符只能确保属性在当前对象中不被修改,但如果该属性本身是一个对象,其内部属性仍可能被修改。需要配合 Readonly 工具类型实现深层只读。
// 使用 readonly 标记 id 属性为只读
interface User {
  readonly id: number;
  name: string;
  age: number;
}

// 创建用户对象
const user: User = {
  id: 1,
  name: "张三",
  age: 25
};

// 可以修改普通属性
user.name = "李四";  // 正常

// 尝试修改只读属性会报错
// user.id = 2;  // 错误:Cannot assign to 'id' because it is a read-only property

4,索引签名

(1)有时候我们无法预先知道对象的所有属性名,但属性的值类型是确定的。比如一个存储翻译文本的对象,键名是语言代码,值是翻译后的文本。这时可以使用 索引签名 来描述这种情况。

(2)下面是索引签名的基本用法:
提示:索引签名有两种形式:字符串索引 [key: string] 和数字索引 [index: number]。同时使用两种索引签名时,数字索引返回的值类型必须是字符串索引返回值类型的子类型。
// 使用索引签名定义可以动态添加字符串属性的对象
interface StringDictionary {
  // 索引签名:属性名是字符串,属性值也是字符串
  [key: string]: string;
}

const dict: StringDictionary = {
  name: "张三",
  city: "北京",
  job: "工程师"
};

// 可以动态添加新属性
dict.email = "zhangsan@example.com";  // 正常

// 错误:属性值类型必须匹配
// dict.age = 25;  // 错误:Type 'number' is not assignable to type 'string'

(3)索引签名也可以标记为只读:
// 只读索引签名:所有属性都不能修改
interface ReadonlyDictionary {
  readonly [key: string]: string;
}

const readonlyDict: ReadonlyDictionary = {
  name: "张三",
  city: "北京"
};

// 正常读取
console.log(readonlyDict.name);  // 输出:张三

// 错误:只读索引签名不允许修改
// readonlyDict.name = "李四";  // 错误:Index signature in type 'ReadonlyDictionary' only permits reading

5,对象类型与 object 类型的区别

(1)在 TypeScript 中,object 类型和对象类型(如 { x: number })是两个不同的概念:
  • object 类型:表示所有非原始类型的值,即任何不是 stringnumberbooleansymbolnullundefined 的值。
  • 对象类型:明确描述了对象的具体结构,包括有哪些属性以及属性的类型。

(2)下面的示例展示了它们的区别:
注意:使用 object 类型时,虽然可以赋值为任何对象,但无法访问其具体属性,因为 TypeScript 不知道对象有什么属性。建议尽量使用具体的对象类型来获得更好的类型检查。
// object 类型:只能赋值为非原始类型
let obj: object;
obj = { name: "张三" };  // 正常
obj = [1, 2, 3];         // 正常(数组也是对象)
obj = function () {};    // 正常(函数也是对象)

// 错误:原始类型不能赋给 object
// obj = "string";  // 错误
// obj = 123;       // 错误
// obj = true;      // 错误

// ============ 分隔线 ============

// 对象类型:明确描述结构
let point: { x: number; y: number };
point = { x: 100, y: 200 };  // 正常

// 错误:缺少属性或类型不匹配
// point = { x: 100 };  // 错误:缺少 y 属性
// point = { x: "100", y: 200 };  // 错误:x 类型不匹配

二、联合类型

1,基本概念

(1)TypeScript 的类型系统允许我们使用多种运算符,从现有类型中构建新类型。其中最常用的一种组合类型的方法就是 联合类型Union Type)。
提示:联合类型非常适合用于描述一个值可能来自多个不同来源的场景,例如:用户 ID 可能是数字编号,也可能是字符串标识,甚至是布尔标记。

(2)联合类型由两个或多个其他类型组成,表示该类型可以是这些类型中的任何一个值。联合类型中的每个类型被称为 联合成员Union Member)。联合类型使用竖线 | 来分隔每个类型。下面是一个联合类型的基本示例:
// 将参数 id 指定为联合类型: number | string | boolean
function printID(id: number | string | boolean) {
  console.log("你的 id 是:", id);
}

// 可以传入 number 类型
printID(123);

// 可以传入 string 类型
printID("abc");

// 可以传入 boolean 类型
printID(true);

export {};

2,类型缩小

(1)当我们获取联合类型的值时,它可能是联合类型中的任何一种类型。由于无法确定具体类型,因此不能直接调用某种类型特有的方法。例如,下面的写法就是错误的:
注意:下面的代码会报错,因为 id 可能是 number 类型,而 number 类型没有 toUpperCase 方法。只有在确定 idstring 类型时才能调用该方法。
// 错误示例:直接调用 string 类型的方法
function printID(id: number | string | boolean) {
  // 错误:Property 'toUpperCase' does not exist on type 'number'
  console.log("你的 id 是:", id.toUpperCase());  // 因为 id 不一定是 string 类型
}

(2)这时需要使用 类型缩小Type Narrowing)技术。TypeScript 会根据代码结构,推断出更具体的类型。常见的类型缩小方式包括:typeof 检查、instanceof 检查、in 操作符、相等性判断等。下面是使用 typeof 进行类型缩小的正确示例:
提示typeof 类型保护只能识别 "number""string""boolean""symbol" 这几种原始类型。对于对象类型,可以使用 instanceofin 操作符进行检查。
function printID(id: number | string | boolean) {
  // 使用联合类型的值时,需要特别小心
  // 通常需要进行类型缩小
  
  if (typeof id === "string") {
    // 类型缩小:TypeScript 知道这里 id 一定是 string 类型
    console.log(id.toUpperCase());  // 正常调用 string 方法
  } else if (typeof id === "number") {
    // TypeScript 知道这里 id 一定是 number 类型
    console.log(id.toFixed(2));  // 正常调用 number 方法
  } else {
    // TypeScript 知道这里 id 一定是 boolean 类型
    console.log(id ? "真" : "假");
  }
}

printID("hello");  // 输出:HELLO
printID(3.14159);  // 输出:3.14
printID(true);    // 输出:真

3,应用场景

(1)联合类型在实际开发中有广泛的应用场景。一个典型例子是处理函数的返回值,返回值可能是正常结果,也可能是错误信息:
// 定义结果类型:可能是成功结果,也可能是错误信息
type Result = string | Error;

function processData(data: string): Result {
  try {
    if (data.length === 0) {
      return new Error("数据为空");
    }
    return `处理成功:${data}`;
  } catch (e) {
    return new Error("处理失败");
  }
}

const result = processData("测试数据");

if (result instanceof Error) {
  console.log("错误:", result.message);
} else {
  console.log(result);
}

(2)另一个常见场景是处理多种状态类型,例如网络请求的不同状态:
提示:下面的示例展示了一种被称为 可辨识联合Discriminated Unions)的模式。每个类型都有一个共同的属性 status,这个属性作为类型的辨识标志,TypeScript 可以根据它的值缩小类型范围。
// 定义网络请求的多种状态类型
type NetworkLoadingState = { status: "loading" };
type NetworkFailedState = { status: "failed"; code: number };
type NetworkSuccessState = { status: "success"; response: string };

// 联合类型:表示网络请求可能处于三种状态之一
type NetworkState = NetworkLoadingState | NetworkFailedState | NetworkSuccessState;

function logger(state: NetworkState): string {
  // 根据状态属性进行类型缩小
  switch (state.status) {
    case "loading":
      return "请求加载中...";
    case "failed":
      return `请求失败,错误码:${state.code}`;  // TypeScript 知道这里有 code 属性
    case "success":
      return `请求成功:${state.response}`;  // TypeScript 知道这里有 response 属性
  }
}

console.log(logger({ status: "loading" }));
console.log(logger({ status: "failed", code: 404 }));
console.log(logger({ status: "success", response: "数据返回成功" }));

(3)此外,在 switch 语句的 default 分支中,将缩小后剩余的类型赋给 never,确保所有情况都被处理。下面是使用 never 类型检查完整性的示例:
提示:这种 never 检查技巧非常有用。如果以后我们添加新的形状类型到 Shape 联合类型中,但忘记在 getArea 函数中添加对应的处理分支,TypeScript 就会在 default 分支报错,提醒我们遗漏了某种情况。
type Shape = "circle" | "square" | "triangle";

function getArea(shape: Shape): number {
  switch (shape) {
    case "circle":
      return Math.PI * 1 * 1;  // 假设半径为 1
    case "square":
      return 1 * 1;  // 假设边长为 1
    case "triangle":
      return 0.5 * 1 * 1;  // 假设底和高为 1
    default:
      // 检查是否所有情况都已处理
      // 如果有遗漏的 case,这里会报错
      const _exhaustiveCheck: never = shape;
      return _exhaustiveCheck;
  }
}

console.log(getArea("circle"));  // 输出:3.14159...

三、类型别名

1,基本概念

(1)前面介绍了在类型注解中编写对象类型和联合类型,如果需要在多个地方重复使用这些类型,就需要多次编写相同的代码。这不仅繁琐,而且容易出错。为了解决这个问题,可以使用 类型别名Type Alias)为某个类型起一个名字。

(2)类型别名使用 type 关键字定义,语法格式为 type 别名名 = 类型定义。定义后,就可以在任何地方使用这个别名来代替原来的类型定义。
提示:类型别名只是给类型起了一个新名字,并不会创建新的类型。它主要用于简化复杂类型的书写,提高代码的可读性和可维护性。

2,为对象类型定义别名

    下面是将前面示例中的对象类型定义为别名。定义了类型别名后,我们可以在多个地方重复使用 PointType,而不需要每次都重新编写完整的对象类型定义。这大大提高了代码的复用性。
// 使用 type 定义类型别名
type PointType = {
  x: number;
  y: number;
  z?: number;  // 可选属性
};

// PointType 是对象类型的别名
function printPoint(point: PointType) {
  console.log(point.x);
  console.log(point.y);
  if (point.z !== undefined) {
    console.log(point.z);
  }
}

// 使用别名简化代码
printPoint({ x: 100, y: 200 });
printPoint({ x: 100, y: 200, z: 300 });

3,为联合类型定义别名

(1)类型别名同样适用于联合类型。下面是为联合类型定义别名的示例:
// IDType 是联合类型 string | number | boolean 的别名
type IDType = string | number | boolean;

// 使用别名简化参数类型定义
function printId(id: IDType) {
  console.log("ID 值为:", id);
}

printId(123);
printId("abc");
printId(true);

(2)在实际项目中,联合类型的别名使用非常广泛。例如,定义用户输入的类型:
提示:下面的 UserRole 类型是一种特殊的联合类型,称为 字面量联合类型。它限制了变量的值只能是几个特定的字符串之一,非常适合用于定义枚举式的常量。
// 定义输入值类型
type InputValue = string | number | null | undefined;

// 定义用户角色类型
type UserRole = "admin" | "user" | "guest";

// 使用别名
function processInput(input: InputValue): string {
  if (input === null || input === undefined) {
    return "空值";
  }
  return String(input);
}

function checkRole(role: UserRole): boolean {
  return role === "admin";
}

console.log(processInput("测试"));  // 输出:测试
console.log(processInput(null));     // 输出:空值
console.log(checkRole("admin"));     // 输出:true
console.log(checkRole("user"));      // 输出:false

4,type 与 interface 的区别

(1)在 TypeScript 中,声明对象类型有两种主要方式:类型别名type)和 接口interface)。它们非常相似,大多数情况下可以互换使用。

(2)两者主要区别如下:
  • 扩展方式不同interface 使用 extends 关键字继承,type 使用 &(交叉类型)扩展。
  • 声明合并interface 支持同名接口自动合并,type 不支持同名合并(会报错)。
  • 类型范围type 可以为任何类型起别名(包括联合类型、元组、原始类型等),interface 只能描述对象类型。
  • 错误提示interface 在错误提示中通常显示接口名,type 可能显示完整的类型结构。

(3)下面展示两者的具体区别示例:
提示:官方建议对于公开需要暴露的类型,优先使用 interface,因为它支持声明合并,可以方便第三方扩展。对于内部类型、联合类型、元组类型等,使用 type 更加灵活。
// interface 的声明合并特性
interface Person {
  name: string;
}

interface Person {
  age: number;
}

// 两个同名 interface 会自动合并
const person: Person = {
  name: "张三",
  age: 25
};

console.log(person);

// ============ 分隔线 ============

// type 不支持声明合并
type Animal = {
  name: string;
};

// 下面这行会报错:Duplicate identifier 'Animal'
// type Animal = {
//   age: number;
// };

// type 使用交叉类型扩展
type AnimalWithAge = Animal & { age: number };

const animal: AnimalWithAge = {
  name: "小狗",
  age: 3
};

console.log(animal);
评论

全部评论(0)

回到顶部