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








一、对象类型
1,基本概念
(1)在 JavaScript 中,我们分组和传递数据的基本方式是通过对象。在 TypeScript 中,我们通过 对象类型 来表示它们。对象类型用于描述具有多个属性的 JavaScript 值,几乎是最常用的类型之一。
(2)定义对象类型的方式很简单,只需列出其属性及其类型。例如,我们希望限定一个函数接收的参数为具有 x 和 y 属性的对象类型,我们可以使用 { x: number; y: number } 作为参数的类型。这个类型描述了一个包含两个属性的对象:x 属性的类型是 number,y 属性的类型也是 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 类型:表示所有非原始类型的值,即任何不是 string、number、boolean、symbol、null、undefined 的值。
- 对象类型:明确描述了对象的具体结构,包括有哪些属性以及属性的类型。
(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 方法。只有在确定 id 是 string 类型时才能调用该方法。
// 错误示例:直接调用 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" 这几种原始类型。对于对象类型,可以使用 instanceof 或 in 操作符进行检查。
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)