📚 TypeScript 教程系列

  1. 入门与配置
  2. 基础类型与变量声明
  3. 函数
  4. 流程控制与运算符
  5. 集合类型
  6. 异步编程与错误处理
  7. 接口与类
  8. 泛型与类型组合(本文)
  9. 高级类型
  10. 模块、装饰器与工程化

⚠️ 来源声明:本文内容参考自 菜鸟教程 TypeScript 教程,仅供学习交流,版权归原作者所有。
TypeScript 的类型系统不止能描述单一类型,还能通过泛型让代码与具体类型解耦,再借助 type 别名、联合类型、交叉类型、字面量类型等把多个类型组合出精确的约束。本篇从泛型出发,依次串联类型别名、联合与交叉、字面量、类型守卫、可选链与空值合并、模板字面量类型,构建一套可复用且类型安全的类型组合工具链。

泛型(Generics)

泛型是一种语言特性,它允许在定义函数、类、接口等时用占位符表示类型而非具体类型。这一功能有助于编写可重用、灵活且类型安全的代码。

其主要目的是处理不特定类型的数据,使代码能适配多种数据类型而不丧失类型检查。

泛型的三大优势

  • 代码重用: 可编写与特定类型无关的通用代码,提升复用性。
  • 类型安全: 编译阶段进行类型检查,规避运行时类型错误。
  • 抽象性: 支持编写更抽象通用的代码,适应不同数据类型和数据结构。

泛型标识符

约定俗成的标识符包括 T(Type)、UV 等,实际上任何合法标识符均可使用。

T — 代表 “Type”,最常见的泛型类型参数名。

1
2
3
function identity<T>(arg: T): T {
return arg;
}

K, V — 表示键(Key)和值(Value)的泛型类型参数。

1
2
3
4
interface KeyValuePair<K, V> {
key: K;
value: V;
}

E — 表示数组元素的泛型类型参数。

1
2
3
function printArray<E>(arr: E[]): void {
arr.forEach(item => console.log(item));
}

R — 表示函数返回值的泛型类型参数。

1
2
3
function getResult<R>(value: R): R {
return value;
}

U, V — 通常表示第二、第三个泛型类型参数。

1
2
3
function combine<U, V>(first: U, second: V): string {
return `${first} ${second}`;
}

这些标识符仅为惯例,实际可选用任何符合命名规范的名称。关键在于代码的可读性,因此建议使用描述性名称以便理解其用途。

泛型函数

使用泛型创建可处理不同类型的函数。

1
2
3
4
5
6
7
8
9
function identity<T>(arg: T): T {
return arg;
}

let result = identity<string>("Hello");
console.log(result);

let numberResult = identity<number>(42);
console.log(numberResult);

上述 identity 是一个泛型函数,通过 <T> 表示泛型类型,参数和返回值均为类型 T。调用时用尖括号 <> 显式指定类型——第一次指定 string,第二次指定 number

泛型接口

使用泛型定义接口,使其成员能适配任意类型。

1
2
3
4
5
6
7
interface Pair<T, U> {
first: T;
second: U;
}

let pair: Pair<string, number> = { first: "hello", second: 42 };
console.log(pair);

此处定义了泛型接口 Pair,带有两个类型参数 TU。创建对象 pair 时,first 为字符串类型,second 为数字类型。

泛型类

泛型同样可应用于类的实例变量和方法。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
class Box<T> {
private value: T;

constructor(value: T) {
this.value = value;
}

getValue(): T {
return this.value;
}
}

let stringBox = new Box<string>("TypeScript");
console.log(stringBox.getValue());

Box 是一个泛型类,使用 <T> 表示泛型类型。构造函数和 getValue 方法均使用泛型类型 T。通过 Box<string> 实例化后创建了存储字符串的 Box 实例。

多类型参数

泛型可以同时接受多个类型参数,用逗号分隔,常用于需要同时处理多种类型的场景。

1
2
3
4
5
6
function merge<T, U>(first: T, second: U): [T, U] {
return [first, second];
}

const merged = merge<string, number>("id", 42);
console.log(merged); // [ 'id', 42 ]
1
2
3
4
5
6
7
interface KeyValuePair<K, V> {
key: K;
value: V;
}

const kv: KeyValuePair<string, boolean> = { key: "enabled", value: true };
console.log(kv);

泛型约束(extends)

有时需要限制泛型的类型范围,可通过约束实现。

1
2
3
4
5
6
7
8
9
10
11
interface Lengthwise {
length: number;
}

function logLength<T extends Lengthwise>(arg: T): void {
console.log(arg.length);
}

logLength("hello");

logLength(42); // 错误

泛型函数 logLength 要求参数类型 T 必须实现 Lengthwise 接口(即具有 length 属性)。因此 logLength("hello") 可正确调用,而 logLength(42) 会报错,因为数字没有 length 属性。

约束也可以引用其他类型参数,例如从对象中安全取属性:

1
2
3
4
5
6
7
function getProperty<T, K extends keyof T>(obj: T, key: K) {
return obj[key];
}

const person = { name: "Alice", age: 25 };
console.log(getProperty(person, "name")); // Alice
// getProperty(person, "phone"); // 错误:"phone" 不是 person 的属性

泛型与默认类型

可为泛型设置默认类型,在不指定类型参数时自动使用默认类型。

1
2
3
4
5
6
function defaultValue<T = string>(arg: T): T {
return arg;
}

let result1 = defaultValue("hello");
let result2 = defaultValue(42);

函数 defaultValue 的泛型参数 T 默认类型为 string。若未显式指定类型则使用默认类型。第一个调用中 result1 被推断为 string,第二个调用中 result2 被推断为 number

默认类型在泛型接口与泛型类型别名中同样适用:

1
2
3
4
5
6
7
interface Response<T = any> {
code: number;
data: T;
}

const r1: Response = { code: 200, data: "ok" }; // T 默认为 any
const r2: Response<number> = { code: 200, data: 1 }; // 显式指定为 number

type 别名

type 别名(Type Alias)用于为现有类型创建别名,让代码更简洁、更易读。通过类型别名,可以为复杂类型定义一个简短的名字,提高代码的可维护性。

type 别名使用 type 关键字定义,它只是为现有类型起了一个新名字,不会创建新的类型。

type 别名工作原理

步骤说明
原始复杂类型type User = { name: string; age: number; email: string; }
定义 type 别名type UserID = User;type ID = string | number;
使用var id: UserID; var uid: ID;

type 别名的应用场景包括:基础类型别名、联合类型别名、函数类型别名、泛型类型别名等。

为什么需要 type 别名

当代码中多次使用同一个复杂类型时,每次都写完整类型会很冗长。type 别名可以为复杂类型定义一个简洁的名字,让代码更易读、更易维护。特别是在使用联合类型、函数类型、元组等复杂类型时,type 别名非常有用。

基本用法

使用 type 关键字为类型定义别名。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
// 类型别名:为联合类型定义别名
// ID 可以是字符串或数字
type ID = string | number;

// 类型别名:为对象类型定义别名
// Point 表示一个坐标点
type Point = { x: number; y: number };

// 使用类型别名
var userId: ID = "123";
var productId: ID = 456;

// 使用 Point 类型别名
var point: Point = { x: 10, y: 20 };

console.log("用户ID: " + userId);
console.log("产品ID: " + productId);
console.log("坐标: " + JSON.stringify(point));

输出:

1
2
3
用户ID: 123
产品ID: 456
坐标: {"x":10,"y":20}

类型别名只是给类型起了个新名字,编译后不会产生实际代码,它完全用于开发时的类型检查。

接口 vs 类型别名

类型别名和接口非常相似,都可以用来定义对象类型,但有一些细微区别。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
// 使用 type 别名定义对象类型
// type 可以定义任何类型,不仅仅是对象
type PersonType = {
name: string;
age: number;
};

// 使用接口定义对象类型
// 接口可以声明合并,可以被类实现
interface PersonInterface {
name: string;
age: number;
}

// 两者都可以用来声明变量类型
var person1: PersonType = { name: "Alice", age: 25 };
var person2: PersonInterface = { name: "Bob", age: 30 };

console.log("PersonType: " + JSON.stringify(person1));
console.log("PersonInterface: " + JSON.stringify(person2));

type 与 interface 的关键区别:

对比项typeinterface
适用范围可定义联合、元组、函数、基本类型等任意类型主要用于定义对象类型
声明合并不支持(同名重复定义会报错)支持(同名接口自动合并)
类实现可以被类 implements可以被类 implements
继承/扩展通过 & 交叉扩展通过 extends 继承
计算属性支持(映射类型、模板字面量等)受限

type 可以定义任何类型(联合类型、元组、函数类型等),而接口主要用于定义对象类型。接口支持声明合并,type 不支持。

类型别名与联合类型

类型别名非常适合定义联合类型,让代码更清晰。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
// 联合类型别名:定义状态的可能值
// Status 只能是这三个字符串字面量之一
type Status = "pending" | "success" | "error";

// 联合类型别名:定义多种可能的返回类型
// Result 可以是字符串、数字或布尔值
type Result = string | number | boolean;

// 使用联合类型别名
function getStatus(status: Status): void {
console.log("状态: " + status);
}

getStatus("success");
getStatus("error");

// 使用 Result 类型
var result: Result = "hello";
result = 42;
console.log("结果: " + result);

输出:

1
2
3
状态: success
状态: error
结果: 42

联合类型别名常用于定义状态码、错误类型、API 返回值等场景。

类型别名与元组

类型别名也可以用于定义元组类型。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
// 元组类型别名:定义坐标
// Coordinate 是一个包含两个数字的元组
type Coordinate = [number, number];

// 元组类型别名:定义姓名和年龄
// NameAge 是一个字符串和数字的元组
type NameAge = [string, number];

// 使用元组类型别名
var coord: Coordinate = [10, 20];
var person: NameAge = ["Alice", 25];

console.log("坐标: " + coord);
console.log("信息: " + person[0] + ", " + person[1]);

元组类型别名让元组的使用更加清晰,避免了每次都需要写出完整元组类型的问题。

类型别名与函数

类型别名可以简化函数类型的声明。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
// 函数类型别名:定义回调函数类型
// Callback 接受一个字符串参数,返回 void
type Callback = (result: string) => void;

// 函数类型别名:定义数学运算函数类型
// MathOperation 接受两个数字参数,返回数字
type MathOperation = (a: number, b: number) => number;

// 使用函数类型别名
var add: MathOperation = function(a, b) { return a + b; };
var multiply: MathOperation = function(a, b) { return a * b; };

console.log("加法: " + add(2, 3));
console.log("乘法: " + multiply(4, 5));

输出:

1
2
加法: 5
乘法: 20

函数类型别名让函数类型声明更简洁,特别是在需要多次使用相同函数签名时。

类型别名与泛型

类型别名可以配合泛型使用,创建可复用的类型定义。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
// 泛型类型别名:定义结果类型
// Result<T> 是一个泛型类型,T 是成功时的数据类型
type Result<T> = { success: boolean; data?: T; error?: string };

// 泛型类型别名:定义键值对类型
// Pair<K, V> 有两个类型参数
type Pair<K, V> = { key: K; value: V };

// 使用泛型类型别名
var result: Result<string> = { success: true, data: "Hello" };
var pair: Pair<string, number> = { key: "age", value: 25 };

console.log("结果: " + JSON.stringify(result));
console.log("键值对: " + JSON.stringify(pair));

输出:

1
2
结果: {"success":true,"data":"Hello"}
键值对: {"key":"age","value":25}

泛型类型别名可以适应不同的数据类型,提高代码的复用性。

类型别名与映射类型

类型别名可以与映射类型结合,创建强大的类型转换。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
// 映射类型:将所有属性变为只读
type Readonly<T> = { readonly [P in keyof T]: T[P] };

// 映射类型:将所有属性变为可选
type Partial<T> = { [P in keyof T]?: T[P] };

// 定义用户接口
interface User {
name: string;
age: number;
}

// 使用映射类型别名
var readonlyUser: Readonly<User> = { name: "Alice", age: 25 };
// readonlyUser.name = "Bob"; // 错误:只读属性不能修改

var partialUser: Partial<User> = { name: "Bob" };

console.log("只读用户: " + JSON.stringify(readonlyUser));
console.log("部分用户: " + JSON.stringify(partialUser));

映射类型是 TypeScript 非常强大的特性,可以基于现有类型创建新的类型变化。

type 别名注意事项

  • 同一个 type 别名不能重复定义(接口可以声明合并)。
  • type 别名在编译后不产生任何代码。
  • 不要滥用类型别名,简洁明了的代码更好。

当类型需要复用,或者类型名称过长时使用 type 别名。对于简单的对象类型,可以根据团队习惯选择 type 或接口。

联合类型(Union Types)

联合类型(Union Types)可以通过管道(|)让变量设置为多种类型,赋值时可以根据设置的类型来赋值。只能赋值指定的类型,如果赋值其它类型就会报错。

语法格式:Type1|Type2|Type3

基本实例

1
2
3
4
5
var val:string|number
val = 12
console.log("数字为 " + val)
val = "Runoob"
console.log("字符串为 " + val)

编译以上代码,得到以下 JavaScript 代码:

1
2
3
4
5
var val;
val = 12;
console.log("数字为 " + val);
val = "Runoob";
console.log("字符串为 " + val);

输出结果为:

1
2
数字为 12
字符串为 Runoob

如果赋值其它类型就会报错,例如 var val:string|number 后写 val = true 会出现编译错误。

联合类型作为函数参数

联合类型也可以作为函数参数使用:

1
2
3
4
5
6
7
8
9
10
11
12
13
function disp(name:string|string[]) {
if(typeof name == "string") {
console.log(name)
} else {
var i;
for(i = 0;i<name.length;i++) {
console.log(name[i])
}
}
}
disp("Runoob")
console.log("输出数组....")
disp(["Runoob","Google","Taobao","Facebook"])

编译以上代码,得到以下 JavaScript 代码:

1
2
3
4
5
6
7
8
9
10
11
12
13
function disp(name) {
if (typeof name == "string") {
console.log(name);
} else {
var i;
for (i = 0; i < name.length; i++) {
console.log(name[i]);
}
}
}
disp("Runoob");
console.log("输出数组....");
disp(["Runoob", "Google", "Taobao", "Facebook"]);

输出结果为:

1
2
3
4
5
6
Runoob
输出数组....
Runoob
Google
Taobao
Facebook

联合类型数组

也可以将数组声明为联合类型:

1
2
3
4
5
6
7
8
9
10
11
12
var arr:number[]|string[];
var i:number;
arr = [1,2,4]
console.log("**数字数组**")
for(i = 0;i<arr.length;i++) {
console.log(arr[i])
}
arr = ["Runoob","Google","Taobao"]
console.log("**字符串数组**")
for(i = 0;i<arr.length;i++) {
console.log(arr[i])
}

编译以上代码,得到以下 JavaScript 代码:

1
2
3
4
5
6
7
8
9
10
11
12
var arr;
var i;
arr = [1, 2, 4];
console.log("**数字数组**");
for (i = 0; i < arr.length; i++) {
console.log(arr[i]);
}
arr = ["Runoob", "Google", "Taobao"];
console.log("**字符串数组**");
for (i = 0; i < arr.length; i++) {
console.log(arr[i]);
}

输出结果为:

1
2
3
4
5
6
7
8
**数字数组**
1
2
4
**字符串数组**
Runoob
Google
Taobao

交叉类型(Intersection Types)

交叉类型(Intersection Types)将多个类型合并成一个新类型,新类型包含所有成员的类型。这类似于面向对象中的多重继承,让一个类型可以拥有多个类型的特性。

交叉类型使用 & 符号连接多个类型,表示新类型包含所有类型的成员。

交叉类型工作原理

类型 A (Person)类型 B (Worker)合并 A & B (Employee)
name: stringcompany: stringname: string
age: numbersalary: numberage: number
company: string
salary: number

交叉类型使用场景:

  • 类型组合 — 合并多个类型特性
  • Mixin 模式 — 组合多个类的功能
  • 替代接口继承 — 更简洁的类型组合

为什么需要交叉类型

在开发中,一个类型往往需要拥有多个类型的特性。例如,一个员工既是一个人(Person),也是一个工作者(Worker),需要同时具有两者的属性。交叉类型让我们可以将多个类型合并成一个,满足这种需求。

基本语法

使用 & 符号组合多个类型。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
// 定义人员类型
interface Person {
name: string;
age: number;
}

// 定义工作者类型
interface Worker {
company: string;
salary: number;
}

// 使用交叉类型合并两个接口
// Employee 类型同时具有 Person 和 Worker 的所有属性
type Employee = Person & Worker;

// 创建同时具有两个类型特性的对象
var employee: Employee = {
name: "Alice",
age: 25,
company: "Google",
salary: 100000
};

console.log("员工: " + JSON.stringify(employee));

输出:

1
员工: {"name":"Alice","age":25,"company":"Google","salary":100000}

交叉类型 A & B 意味着新类型同时具有 A 和 B 的所有属性,缺一不可。

交叉类型与接口继承

交叉类型可以替代接口的多重继承。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
// 定义类型 A
interface A {
a: string;
}

// 定义类型 B
interface B {
b: number;
}

// 使用接口继承多个接口
interface AB extends A, B {
c: boolean;
}

// 使用交叉类型(更简洁)
type ABType = A & B & { c: boolean };

// 两种方式都能创建包含所有属性的类型
var obj: ABType = { a: "hello", b: 42, c: true };
console.log("对象: " + JSON.stringify(obj));

输出:

1
对象: {"a":"hello","b":42,"c":true}

交叉类型比接口继承更简洁,特别是当需要继承多个类型且需要添加额外属性时。

类型混合(Mixin 模式)

使用交叉类型实现 Mixin 模式,可以动态组合类的功能。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
// 定义构造函数类型
type Constructor = new (...args: any[]) => {};

// Mixin:添加时间戳功能
function Timestamped<T extends Constructor>(Base: T) {
return class extends Base {
timestamp = Date.now();
};
}

// Mixin:添加序列化功能
function Serializable<T extends Constructor>(Base: T) {
return class extends Base {
serialize() {
return JSON.stringify(this);
}
};
}

// 基础用户类
class User {
name: string;
constructor(name: string) {
this.name = name;
}
}

// 组合 Mixin
var TimestampedUser = Timestamped(User);
var SerializableUser = Serializable(User);
var FullUser = Serializable(Timestamped(User));

var user = new FullUser("Alice");
console.log("时间戳: " + user.timestamp);
console.log("序列化: " + user.serialize());

Mixin 是一种强大的模式,可以在不修改原有类的情况下为其添加新功能。

交叉类型与联合类型

交叉类型和联合类型结合时需要特别注意优先级。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
// 联合类型:可以是字符串或数字
type StringOrNumber = string | number;

// 交叉类型:不兼容类型的交叉
// string & number = never(没有类型同时是字符串和数字)
type Both = string & number;

// 定义三个类型
type A = { a: string };
type B = { b: number };
type C = { c: boolean };

// 联合类型与交叉类型结合
// (A | B) & C 会将联合类型的每个分支都与 C 交叉
type Combined = (A | B) & C;

// 实际结果是:{ a: string; c: boolean } | { b: number; c: boolean }
var obj: Combined = { a: "hello", c: true };
console.log("组合: " + JSON.stringify(obj));

输出:

1
组合: {"a":"hello","c":true}

不兼容的类型进行交叉会得到 never 类型。例如 string & number 是无效的。

交叉类型注意事项

  • 不兼容类型: 交叉不兼容的类型会得到 never。
  • 优先级: 联合类型的优先级高于交叉类型。
  • 方法冲突: 如果两个类型有同名的方法,需要手动处理冲突。

交叉类型适合组合多个接口或类型,当需要多重继承时,优先使用交叉类型而非接口继承。

字面量类型(Literal Types)

字面量类型(Literal Types)是 TypeScript 中一种表示特定值而不是通用类型的特性。它允许我们将变量类型限制为具体的值,而不是宽泛的 string、number 等类型。这提供了更精确的类型控制,使代码更加类型安全。

字面量类型对比

写法可赋值范围
var direction: string;任何字符串 “up”、“down”、“abc”…
var direction: "up" | "down" | "left" | "right"只能赋这四个值之一
赋值 direction = "upup"编译错误!

字面量类型种类:

  • 字符串字面量"up" | "down" | "left" — 限制特定字符串
  • 数字字面量1 | 2 | 3 | 100 | 200 — 限制特定数字
  • 布尔字面量true | false — true 或 false
  • 模板字面量`on${string}` — 动态生成

字面量类型是泛型类型(string、number)的具体值。例如 "up"string 的子类型,只能赋值给它的值是特定的字符串。

字符串字面量类型

字符串字面量类型将变量的取值限制为特定的字符串。这在需要限制变量只能取某些固定值时非常有用。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
// 定义字符串字面量类型
// direction 只能是四个特定值之一
var direction: "up" | "down" | "left" | "right";

// 正确:赋值为字面量类型中的值
direction = "up";

// 错误:赋值不在列表中的值
// direction = "upup"; // 编译错误!

console.log("方向: " + direction);

// 使用类型别名创建可复用的字面量类型
type Status = "pending" | "active" | "completed";

var currentStatus: Status = "active";
console.log("状态: " + currentStatus);

输出:

1
2
方向: up
状态: active

字符串字面量常用于定义枚举值、状态码、配置选项等需要限制为特定值的场景。

数字字面量类型

数字字面量类型将变量的取值限制为特定的数字。这在需要使用特定数值(如状态码、错误码)时非常有用。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
// 定义数字字面量类型
var code: 200 | 404 | 500;

code = 200;

// 错误:赋值不在列表中的值
// code = 301; // 编译错误!

console.log("状态码: " + code);

// 使用数字字面量模拟枚举
type Weekday = 1 | 2 | 3 | 4 | 5 | 6 | 7;

var today: Weekday = 1;
console.log("今天是星期: " + today);

数字字面量可以替代简单的枚举(enum)使用,尤其在只需要几个固定数值时。

布尔字面量类型

布尔字面量类型将变量的取值限制为 true 或 false。实际上,TypeScript 中的 boolean 类型就是 true | false 的别名。

1
2
3
4
5
6
7
8
9
// 定义布尔字面量类型
var isActive: true | false;

isActive = true;

// 实际上 boolean 就是 true | false 的联合类型
var flag: boolean = true;

console.log("是否激活: " + isActive);

虽然 boolean 本身已经足够使用,但在需要明确区分 true 和 false 的类型时,可以使用 truefalse 作为类型。

对象字面量类型

对象字面量类型可以定义对象的结构,并可以使用 readonly 修饰符将属性设为只读。这在需要创建不可变对象时非常有用。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
// 定义对象字面量类型
type Point = {
x: number;
y: number;
};

var p: Point = { x: 10, y: 20 };
console.log("点: " + JSON.stringify(p));

// 定义只读对象类型
type ReadonlyPoint = {
readonly x: number;
readonly y: number;
};

var rp: ReadonlyPoint = { x: 1, y: 2 };

// 尝试修改只读属性会报错
// rp.x = 3; // 编译错误:只读属性不能修改

console.log("只读点: " + JSON.stringify(rp));

输出:

1
2
: {"x":10,"y":20}
只读点: {"x":1,"y":2}

只读对象常用于定义配置对象、常量对象等不希望被修改的数据。

字面量类型与类型推断

TypeScript 会根据变量的声明方式自动推断为字面量类型。使用 const 声明的变量会被推断为具体的字面量类型,而不是宽泛的类型。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
// 使用 var 声明并赋值
// TypeScript 会推断为宽泛类型
var name = "Alice"; // 类型: string(因为 var 可以重新赋值)
var age = 25; // 类型: number
var enabled = true; // 类型: boolean

console.log("名字: " + name + ", 年龄: " + age + ", 启用: " + enabled);

// 使用 as const 创建深度只读字面量
var colors = ["red", "green", "blue"] as const;

// colors 的类型变为:
// readonly ["red", "green", "blue"]

console.log("颜色: " + colors[0]);

// 尝试修改会报错
// colors[0] = "yellow"; // 编译错误:只读

as const 会将类型推断为最具体的字面量类型,并添加 readonly 修饰符。

实际应用:Redux Action

字面量类型在实际开发中有广泛的应用。下面展示如何使用字面量类型实现 Redux 风格的行为(Action)。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
// 定义 Redux 风格的 Action 类型
type Action =
| { type: "increment"; payload: number }
| { type: "decrement"; payload: number }
| { type: "reset" };

function reducer(action: Action): void {
switch (action.type) {
case "increment":
// 在这个分支中,action 被推断为 { type: "increment"; payload: number }
console.log("增加: " + action.payload);
break;
case "decrement":
console.log("减少: " + action.payload);
break;
case "reset":
console.log("重置");
break;
}
}

reducer({ type: "increment", payload: 5 });
reducer({ type: "reset" });

输出:

1
2
增加: 5
重置

这种模式叫做 “可辨识联合”(Discriminated Union),是 TypeScript 中处理多种相关类型的最佳实践。

字面量类型注意事项

  • 类型收窄: 使用 switch 或 if 判断时,TypeScript 会自动收窄字面量类型。
  • 类型别名: 建议使用 type 别名为常用的字面量类型创建别名。
  • const 推断: const 声明的变量会自动推断为字面量类型。
  • as const: 需要深度只读时使用 as const。

在需要限制变量只能取特定值时,优先使用字面量类型而不是枚举。

类型守卫(Type Guards)

类型守卫(Type Guards)是 TypeScript 中非常重要的一种类型缩小机制。它允许开发者在运行时通过特定的条件检查,让 TypeScript 编译器能够准确推断出变量的具体类型。通过类型守卫,我们可以安全地访问联合类型变量中特定类型的属性和方法。

为什么需要类型守卫

在 TypeScript 中,一个变量可能被声明为多种类型的联合。当我们需要根据不同类型执行不同操作时,编译器无法自动判断当前的具体类型。类型守卫就是解决这个问题的关键机制。

类型守卫的核心原理是 “类型缩窄”(Type Narrowing):通过条件判断,TypeScript 会自动将联合类型缩小为具体的类型。

类型守卫方式总览

类型守卫方式用法示例
typeoftypeof x === "string"
instanceofx instanceof Array
自定义守卫x is String / value is Type
in 属性检查"prop" in x

typeof 类型守卫

typeof 是最常用的类型守卫,用于检查原始类型(string、number、boolean 等)。它返回一个字符串,表示值的类型。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
// 定义一个接收联合类型的函数
function printValue(value: string | number): void {
// 使用 typeof 检查类型
if (typeof value === "string") {
// 此时 TypeScript 知道 value 是 string
console.log("字符串长度: " + value.length);
} else {
// 进入 else 分支时,TypeScript 知道 value 是 number
console.log("数字翻倍: " + (value * 2));
}
}

printValue("hello");
printValue(42);

输出:

1
2
字符串长度: 5
数字翻倍: 84

typeof 支持的类型:

  • “string” — 字符串类型
  • “number” — 数字类型(包括 NaN 和 Infinity)
  • “boolean” — 布尔类型
  • “undefined” — 未定义类型
  • “object” — 对象类型(注意:数组和 null 也会被识别为 “object”)
  • “function” — 函数类型

typeof 对于数组和 null 都会返回 “object”。如果需要精确区分数组和对象,需要使用其他方式。

instanceof 类型守卫

instanceof 用于检查对象是否是某个类的实例。它通过检查对象的原型链来判断类型。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
class Dog {
bark(): void {
console.log("汪汪汪!");
}
}

class Cat {
meow(): void {
console.log("喵喵喵!");
}
}

function makeSound(animal: Dog | Cat): void {
if (animal instanceof Dog) {
animal.bark();
} else {
animal.meow();
}
}

makeSound(new Dog());
makeSound(new Cat());

输出:

1
2
汪汪汪!
喵喵喵!

instanceof 检查的是对象的原型链,因此它只能用于类实例,不能用于接口和类型别名。

自定义类型守卫

当内置的 typeof 和 instanceof 无法满足需求时,可以创建自定义类型守卫函数。自定义类型守卫使用 value is Type 的返回类型语法。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
// 定义一个自定义类型守卫函数
// 返回类型使用 "value is Type" 格式
function isString(value: any): value is string {
return typeof value === "string";
}

function isNumber(value: any): value is number {
return typeof value === "number";
}

function isArray(value: any): value is any[] {
return Array.isArray(value);
}

function processValue(value: string | number | any[]): void {
if (isString(value)) {
console.log("字符串转大写: " + value.toUpperCase());
} else if (isNumber(value)) {
console.log("数字格式化: " + value.toFixed(2));
} else if (isArray(value)) {
console.log("数组长度: " + value.length);
}
}

processValue("hello");
processValue(3.14159);
processValue([1, 2, 3, 4, 5]);

输出:

1
2
3
字符串转大写: HELLO
数字格式化: 3.14
数组长度: 5

自定义类型守卫的关键是返回类型 value is Type,这是 TypeScript 识别类型守卫的标志。

in 操作符类型守卫

in 操作符可以检查对象是否包含某个属性。在条件判断中使用 in,TypeScript 会自动缩小对象的类型范围。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
interface A {
a: string;
}

interface B {
b: number;
}

function process(obj: A | B): void {
if ("a" in obj) {
// 在 if 分支中,obj 的类型被缩小为 A
console.log("A 的属性 a: " + obj.a);
} else {
// else 分支中,obj 只能是 B 类型
console.log("B 的属性 b: " + obj.b);
}
}

process({ a: "hello" });
process({ b: 42 });

输出:

1
2
A 的属性 a: hello
B 的属性 b: 42

可辨识联合与类型守卫

可辨识联合是一种强大的模式,它通过一个公共的 “标识” 属性来区分联合类型成员。结合 switch 语句或 if 判断,可以实现完整的类型守卫。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
interface Circle {
kind: "circle"; // 标识字段
radius: number;
}

interface Rectangle {
kind: "rectangle"; // 标识字段
width: number;
height: number;
}

interface Triangle {
kind: "triangle"; // 标识字段
base: number;
height: number;
}

type Shape = Circle | Rectangle | Triangle;

function getArea(shape: Shape): number {
switch (shape.kind) {
case "circle":
// shape 被缩小为 Circle 类型
return Math.PI * shape.radius ** 2;
case "rectangle":
return shape.width * shape.height;
case "triangle":
return 0.5 * shape.base * shape.height;
}
}

var circle = { kind: "circle" as const, radius: 5 };
var rectangle = { kind: "rectangle" as const, width: 4, height: 6 };
var triangle = { kind: "triangle" as const, base: 3, height: 4 };

console.log("圆形面积: " + getArea(circle).toFixed(2));
console.log("矩形面积: " + getArea(rectangle));
console.log("三角形面积: " + getArea(triangle));

输出:

1
2
3
圆形面积: 78.54
矩形面积: 24
三角形面积: 6

可辨识联合是 TypeScript 中最推荐使用的模式之一。它通过一个公共的字面量属性(通常是 kindtype)来区分不同的类型成员,使代码既类型安全又易于维护。

null 和 undefined 检查

处理可能为 null 或 undefined 的值时,直接的相等性检查也是有效的类型守卫。

1
2
3
4
5
6
7
8
9
10
function getLength(str: string | null): number {
// 直接检查 str 不等于 null
if (str !== null) {
return str.length;
}
return 0;
}

console.log(getLength("hello"));
console.log(getLength(null));

输出:

1
2
5
0

在启用 strictNullChecks 后,建议始终进行 null 检查。可以使用可选链(?.)和空值合并(??)来简化代码。

真值缩小

除了显式的类型检查,TypeScript 还会通过真值断言(Truthiness)来缩小类型范围。

1
2
3
4
5
6
7
function greet(name?: string): string {
// 使用短路运算符:如果 name 为 undefined 或空字符串,使用默认值
return name && "Hello, " + name;
}

console.log(greet("RUNOOB"));
console.log(greet());

输出:

1
2
Hello, RUNOOB
Hello, undefined

类型守卫注意事项

  • 类型守卫必须在条件分支中使用: 只有在使用类型守卫进行条件判断后,TypeScript 才会进行类型缩小。
  • 返回类型必须是类型谓词: 自定义类型守卫的返回类型必须是 value is Type 格式。
  • 可辨识联合是最佳实践: 对于复杂的联合类型,建议使用可辨识联合模式。
  • 注意类型收窄的完整性: 使用 switch 语句时,建议处理所有可能的分支。

在处理联合类型时,优先考虑使用可辨识联合模式。它不仅代码更清晰,还能充分利用 TypeScript 的类型推断能力。

可选链 ?. 与空值合并 ??

可选链(Optional Chaining)是 TypeScript 和 JavaScript 中一种安全的属性访问方式。它允许开发者以链式调用的方式安全地访问嵌套对象属性。当访问路径中的任意一个属性为 null 或 undefined 时,整个表达式会短路返回 undefined,而不会抛出错误。这极大地简化了深层嵌套对象的属性访问代码。

可选链工作原理

传统方式(冗长): 需要多层检查:var city = user && user.address && user.address.city; — 容易出错,代码冗余。

可选链方式(简洁): 一行搞定:var city = user?.address?.city; — 安全简洁,自动短路。

可选链的三种形式:

  • obj?.prop — 属性访问
  • arr?.[0] — 数组访问
  • obj?.method() — 方法调用

可选链的核心是 “短路求值”:当链中某个属性的值为 null 或 undefined 时,整个表达式的结果立即返回 undefined,而不会继续访问后续属性。

基本语法

使用 ?. 运算符安全访问可能不存在的属性。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
// 定义一个嵌套的用户对象
var user = {
name: "RUNOOB",
address: {
city: "Beijing"
}
};

// 传统方式:使用 && 逐层检查
var city1 = user && user.address && user.address.city;

// 可选链方式:使用 ?. 运算符
var city2 = user?.address?.city;

console.log("传统方式: " + city1);
console.log("可选链: " + city2);

输出:

1
2
传统方式: Beijing
可选链: Beijing

当对象属性存在时,两种方式的结果相同。但可选链的代码更简洁,更易读。

处理不存在的属性

当访问路径中的属性不存在时,可选链会安全地返回 undefined,而不会抛出错误。这对于处理来自 API 的数据或用户表单输入特别有用。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
// 定义一个不完整用户对象
var user = {
name: "RUNOOB"
// address 属性不存在
};

// 使用可选链访问深层属性
var city = user?.address?.city;

// 访问更深的嵌套属性
var country = user?.address?.country?.name;

console.log("城市: " + city);
console.log("国家: " + country);

输出:

1
2
城市: undefined
国家: undefined

可选链只会在属性访问时返回 undefined,不会创建新的对象或属性。

可选链与数组结合

可选链可以与数组下标访问结合使用,安全地访问数组中的元素。使用 ?.[index] 语法,可以在数组元素不存在时返回 undefined。

1
2
3
4
5
6
7
8
9
10
11
12
13
var users = [
{ name: "Alice" },
{ name: "Bob" }
];

// 安全访问数组第一个元素的名字
var firstUser = users?.[0]?.name;

// 安全访问数组中不存在的元素
var tenthUser = users?.[9]?.name;

console.log("第一个用户: " + firstUser);
console.log("第十个用户: " + tenthUser);

输出:

1
2
第一个用户: Alice
第十个用户: undefined

?.[index]?. 的区别在于:前者用于数组,后者用于对象属性。

可选链与方法调用

可选链可以用于安全地调用可能不存在的方法。使用 ?.() 语法,如果方法不存在则返回 undefined,而不会抛出错误。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
var user = {
name: "Alice",
greet: function() {
return "Hello, " + this.name;
}
};

// 安全调用存在的方法
var message1 = user.greet?.();

// 安全调用不存在的方法
var message2 = user.sayHello?.();

console.log("greet: " + message1);
console.log("sayHello: " + message2);

输出:

1
2
greet: Hello, Alice
sayHello: undefined

这在处理可选的回调函数或事件处理程序时特别有用。

可选链赋值

可选链也可以用于赋值操作,但需要注意其行为:可选链赋值只会修改已存在的路径,不会自动创建中间对象。

1
2
3
4
5
6
7
8
var user = {
name: "Alice"
};

// user?.address 不存在,所以整个赋值被跳过
user?.address?.city = "Beijing";

console.log("用户: " + JSON.stringify(user));

输出:

1
用户: {"name":"Alice"}

可选链赋值不能用于创建新属性。如果需要创建嵌套对象,应该使用传统方式先创建中间对象。

空值合并运算符 ??

可选链经常与空值合并运算符 ?? 结合使用。这种组合可以在属性不存在时提供默认值,使代码更加健壮。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
var user = {
name: "Alice"
// address 不存在
};

// 可选链 + 空值合并:当 city 为 null 或 undefined 时使用默认值
var city = user?.address?.city ?? "未知城市";

console.log("城市: " + city);

// 对比:传统方式的复杂写法
var country = user && user.address && user.address.country
? user.address.country
: "未知国家";

console.log("国家: " + country);

输出:

1
2
城市: 未知城市
国家: 未知国家

??|| 的关键区别:

运算符触发默认值的条件示例
??值为 nullundefined0 ?? "默认"0
``

?? 只在值为 null 或 undefined 时使用默认值,而 || 会在值为 falsy(0、“”、false)时也使用默认值。在处理数字或字符串时,应优先使用 ??

可选链注意事项

  • 短路求值: 可选链遇到 null 或 undefined 会立即返回,不会继续访问后续属性。
  • 不能创建属性: 可选链赋值不会自动创建中间对象。
  • 与空值合并配合: 建议始终使用 ?? 提供默认值,而少用 ||
  • 性能考虑: 虽然可选链更安全,但在属性一定存在的情况下,直接访问性能更好。

对于来自外部数据(如 API 响应、用户输入)的属性,优先使用可选链进行安全访问。结合空值合并运算符,可以写出既安全又简洁的代码。

模板字面量类型(Template Literal Types)

模板字面量类型基于字符串字面量类型构建,支持通过插值生成新的字符串类型。这让 TypeScript 能够对字符串进行更精确的类型检查,适用于事件名、路径、类名等场景。

模板字面量类型工作原理

输入类型模板输出类型
type Event = "click" | "focus"`on${Capitalize<Event>}`"onClick" "onFocus"
type Method = "get" | "post"`${Method}:/${string}`"get:/..." "post:/..."

内置工具类型:

工具类型作用示例
Uppercase全部大写"hello""HELLO"
Lowercase全部小写"HELLO""hello"
Capitalize首字母大写"hello""Hello"
Uncapitalize首字母小写"Hello""hello"

为什么需要模板字面量类型

在开发中,我们经常需要处理格式化的字符串,如事件名(onClick)、API 路径(get:/users)、CSS 类名(btn-primary-md)等。使用普通的 string 类型无法精确描述这些格式,而模板字面量类型让我们可以精确地定义这些字符串的类型,大大增强了 TypeScript 的类型安全性,减少了运行时错误。

模板字面量类型使用反引号(`)和 ${} 插值语法来定义字符串类型,类似于 JavaScript 的模板字符串,但用在类型层面。

基本语法

1
2
3
4
5
6
7
8
9
10
// 定义基础字符串字面量类型
type World = "world";

// 使用模板字面量类型
// `Hello ${World}` 相当于 "Hello world"
type Greeting = `Hello ${World}`;

// 只能赋值符合类型定义的字符串
var greeting: Greeting = "Hello world";
console.log("问候: " + greeting);

输出:

1
问候: Hello world

模板字面量类型会将插值的位置替换为实际的字符串,生成新的字面量类型。

内置工具类型

TypeScript 提供了四个内置的工具类型来处理字符串大小写。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
// Uppercase:将字符串转为大写
type UpperHello = Uppercase<"hello">; // "HELLO"

// Lowercase:将字符串转为小写
type LowerHELLO = Lowercase<"HELLO">; // "hello"

// Capitalize:将字符串首字母大写
type CapitalizedHello = Capitalize<"hello">; // "Hello"

// Uncapitalize:将字符串首字母小写
type UncapitalizedHello = Uncapitalize<"Hello">; // "hello"

console.log("Uppercase: " + UpperHello);
console.log("Lowercase: " + LowerHELLO);
console.log("Capitalize: " + CapitalizedHello);
console.log("Uncapitalize: " + UncapitalizedHello);

输出:

1
2
3
4
Uppercase: HELLO
Lowercase: hello
Capitalize: Hello
Uncapitalize: hello

这些工具类型在处理事件名、方法名等需要统一格式的场景非常有用。

事件类型

使用模板字面量类型可以精确地定义事件名称的类型。

1
2
3
4
5
6
7
8
9
10
11
// 构建事件名类型
type EventName = `on${Capitalize<string>}`;
// 构建处理器名称类型
type Handler = `handle${Capitalize<string>}`;

var clickEvent: EventName = "onClick";
var focusEvent: EventName = "onFocus";
var handler: Handler = "handleSubmit";

console.log("事件: " + clickEvent);
console.log("处理器: " + handler);

输出:

1
2
事件: onClick
处理器: handleSubmit

使用模板字面量类型后,像 onclick(小写)这样的错误格式会被 TypeScript 拒绝。

路径类型

使用模板字面量类型可以精确地定义 API 路径的类型。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
// 定义 HTTP 方法类型
type HttpMethod = "get" | "post" | "put" | "delete";

// 定义路径格式
type ApiEndpoint = `/${string}`; // 以斜杠开头的字符串

// 组合成完整的 API 路径类型
type ApiPath = `${HttpMethod}${ApiEndpoint}`;

var getUsers: ApiPath = "get/users";
var createUser: ApiPath = "post/users";

console.log("路径: " + getUsers);
console.log("路径: " + createUser);

输出:

1
2
路径: get/users
路径: post/users

users(没有方法前缀)这样的路径会被 TypeScript 报错。

复杂示例:组合多个联合类型

模板字面量类型可以组合多个联合类型,生成所有可能的组合。

1
2
3
4
5
6
7
8
9
10
11
// 带数字的模板类型
type Row = `row${number}`; // row0, row1, row2...

// 组合多个类型
type Variant = "primary" | "secondary";
type Size = "sm" | "md" | "lg";
// 这会生成 6 种组合:btn-primary-sm, btn-primary-md, btn-primary-lg...
type ClassName = `btn-${Variant}-${Size}`;

var className: ClassName = "btn-primary-md";
console.log("类名: " + className);

输出:

1
类名: btn-primary-md

模板字面量类型会自动展开所有组合。如果联合类型有很多选项,生成的类型可能会非常庞大。

自定义工具类型

可以创建自己的模板字面量工具类型。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
// 添加前缀的工具类型
type Prefix<T extends string, P extends string> = `${P}${Capitalize<T>}`;

// 添加后缀的工具类型
type Suffix<T extends string, S extends string> = `${Capitalize<T>}${S}`;

type HandlerName = Prefix<"click", "on">;
type ButtonId = Suffix<"submit", "Btn">;

var handler: HandlerName = "onClick";
var id: ButtonId = "SubmitBtn";

console.log("处理器: " + handler);
console.log("ID: " + id);

输出:

1
2
处理器: onClick
ID: SubmitBtn

模板字面量类型可以与泛型结合,创建可复用的工具类型。

与 keyof 组合

模板字面量类型常与 keyof、映射类型配合,生成基于对象键的新类型,例如把属性名重写为 getter/setter 形式。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
type Getters<T> = {
[K in keyof T as `get${Capitalize<string & K>}`]: () => T[K];
};

interface Person {
name: string;
age: number;
}

type PersonGetters = Getters<Person>;
// 等价于:
// {
// getName: () => string;
// getAge: () => number;
// }

var getters: PersonGetters = {
getName: () => "Alice",
getAge: () => 25
};

console.log("名字: " + getters.getName());
console.log("年龄: " + getters.getAge());

输出:

1
2
名字: Alice
年龄: 25

通过 as 子句配合模板字面量类型,可以在映射类型中重写键名,从而派生出 get/set/on 等约定式 API 类型。

模板字面量类型注意事项

  • 插值类型: 模板中的 ${} 可以是具体字符串、联合类型、string、number 等。
  • 组合数量: 组合多个联合类型时,生成的类型可能非常大。
  • 大小写处理: 使用内置工具类型处理字符串大小写。

使用模板字面量类型处理事件名、路径、类名等有固定格式的字符串,可以获得更好的类型安全。在需要格式化字符串的场景,优先使用模板字面量类型来获得编译期的类型检查。

小结

本篇从泛型出发,串联起 TypeScript 类型组合的核心能力:

  • 泛型 通过类型参数 <T> 让函数、接口、类与具体类型解耦,配合 extends 约束和默认类型实现可复用的类型安全代码。
  • type 别名 为复杂类型起简洁名字,能表达联合、元组、函数、泛型与映射类型,与 interface 在对象类型上各有侧重。
  • 联合类型| 表示 “或”,交叉类型用 & 表示 “且”,二者可叠加出灵活的类型组合,注意不兼容交叉会得到 never
  • 字面量类型 把取值限定为具体值,配合 as const 与可辨识联合,是实现精确状态建模的关键。
  • 类型守卫 通过 typeofinstanceofin、自定义 value is Type 与真值缩小,在条件分支中安全地缩窄类型。
  • 可选链 ?. 与空值合并 ?? 让深层属性访问与默认值处理既安全又简洁,优于传统的 &&||
  • 模板字面量类型 在类型层面拼接字符串,配合 Uppercase/Lowercase/Capitalize/Uncapitalizekeyof 映射,可生成事件名、路径、getter 等约定式类型。

掌握这些类型组合工具,就能在不牺牲运行时性能的前提下,让编译器帮你挡住大量低级错误,写出既灵活又严谨的 TypeScript 代码。