TypeScript 的类型系统是它最核心的价值所在。理解了这套类型系统,你就能在编写代码时获得智能提示、编译期错误检查和更清晰的自文档化能力。本节将从最常用的类型概念出发,逐步介绍它们的含义、写法及实际应用场景。
22.2.1 基础类型:为 JavaScript 披上类型外衣
TypeScript 为 JavaScript 中的所有基础数据类型都提供了对应的类型标注,同时扩展了几个常用类型。
// 布尔值、数字、字符串 —— 与 JS 完全对应
let isDone: boolean = false;
let age: number = 25;
let name: string = "Alice";
// 数组 —— 两种写法等价
let list1: number[] = [1, 2, 3];
let list2: Array<number> = [1, 2, 3];
// 元组 —— 固定长度和类型的数组
let tuple: [string, number] = ["hello", 10];
// 枚举 —— 为数值或字符串定义友好名称
enum Color { Red, Green, Blue }
let c: Color = Color.Green;
// any —— 绕过类型检查(慎用,会丧失类型保护)
let notSure: any = 4;
notSure = "maybe a string";
// void —— 表示没有返回值,常用于函数
function warnUser(): void {
console.log("This is a warning");
}
// null 和 undefined 是所有类型的子类型(在严格模式下需显式联合)
let u: undefined = undefined;
let n: null = null;
// never —— 表示永远不会出现的值,如抛出异常或死循环
function error(message: string): never {
throw new Error(message);
}
实际经验:在日常开发中,除了 any 应尽量避免外,上述基础类型基本覆盖了 90% 的变量声明场景。不需要刻意去记忆,IDE 会自动提示。
22.2.2 联合类型:让变量支持多种类型
联合类型允许一个值属于几种类型之一,用竖线 | 分隔。
let id: number | string;
id = 101; // 合法
id = "A102"; // 合法
id = true; // 错误
// 常用于函数参数
function printId(id: number | string) {
// 需要使用类型收窄才能安全使用
if (typeof id === "string") {
console.log(id.toUpperCase());
} else {
console.log(id.toFixed(2));
}
}
字面量联合类型 更是常见,用于限制取值集合:
type Direction = "north" | "south" | "east" | "west";
function move(direction: Direction) { /* ... */ }
move("north"); // OK
move("up"); // 错误
这种模式在定义状态、事件名称、配置选项时非常实用,它比单独使用字符串类型提供了精确的自动补全和错误防范。
22.2.3 接口(interface):描述对象形状
接口是 TypeScript 中定义对象结构的主要方式。它只关心值的“形状”,不关心实现细节。
interface User {
name: string;
age: number;
readonly id: number; // 只读属性,创建后不可修改
email?: string; // 可选属性
[key: string]: any; // 索引签名,允许任意额外属性
}
const user: User = {
name: "Bob",
age: 30,
id: 1,
email: "bob@example.com",
nickname: "Bobby" // 索引签名允许
};
// 函数类型的接口
interface SearchFunc {
(source: string, subString: string): boolean;
}
与 type(类型别名)的区别在于:interface 可以被继承、被合并声明,更适合定义公开的 API 或需要扩展的结构。type 则更擅长处理联合类型、元组等。
22.2.4 泛型(Generics):让类型也可以“参数化”
泛型是 TypeScript 中最强大的抽象工具。它允许你在定义函数、接口、类时不预先指定具体的类型,而是作为“类型变量”使用,在调用时再确定。
function identity<T>(arg: T): T {
return arg;
}
let output1 = identity<string>("myString"); // 明确指定 T = string
let output2 = identity(100); // 编译器自动推断 T = number
泛型约束:通过 extends 限制类型变量的范围。
interface Lengthwise {
length: number;
}
function loggingIdentity<T extends Lengthwise>(arg: T): T {
console.log(arg.length);
return arg;
}
loggingIdentity("hello"); // OK,字符串有 length
loggingIdentity([1, 2]); // OK,数组有 length
loggingIdentity(10); // 错误,数字没有 length
泛型在 React 组件、数据模型、工具函数中大量使用,是构建可复用、类型安全代码的核心手段。
22.2.5 工具类型(Utility Types):开箱即用的类型转换
TypeScript 内置了一套工具类型,用于对现有类型进行转换。它们基于泛型实现,非常实用。
| 工具类型 | 作用 | 示例 |
|---------|------|-----|
| Partial<T> | 将 T 的所有属性变为可选 | Partial<User> |
| Required<T> | 将 T 的所有属性变为必填 | Required<Partial<User>> |
| Readonly<T> | 将 T 的所有属性变为只读 | Readonly<User> |
| Pick<T, K> | 从 T 中选取一组属性构造新类型 | Pick<User, 'name' \| 'age'> |
| Omit<T, K> | 从 T 中删除一组属性后构造新类型 | Omit<User, 'id'> |
| Record<K, T> | 构造一个对象类型,属性键为 K,值为 T | Record<'home' \| 'about', string> |
| Exclude<T, U> | 从联合类型 T 中排除 U | Exclude<number \| string, string> → number |
| Extract<T, U> | 从联合类型 T 中提取 U | Extract<number \| string, string> → string |
| NonNullable<T> | 从 T 中排除 null 和 undefined | NonNullable<number \| null> → number |
| ReturnType<T> | 获取函数类型的返回类型 | ReturnType<() => string> → string |
interface Todo {
title: string;
description: string;
completed: boolean;
}
// 更新时只需要部分字段
function updateTodo(id: number, fields: Partial<Todo>) {
// ...
}
// 从实体中挑选信息展示
type TodoPreview = Pick<Todo, "title" | "completed">;
const preview: TodoPreview = {
title: "Learn TypeScript",
completed: false
};
工具类型让我们可以“操作”类型,就像操作值一样。在大型项目中,它们能显著减少重复的类型定义,并保证修改原始类型时派生类型自动同步。
22.2.6 类型系统学习建议
- 从基础类型开始,覆盖日常变量和函数的标注。
- 尽早习惯联合类型和字面量类型,它们是 TypeScript 精简代码、提升安全性的秘诀。
- 掌握接口和类型别名的区别,在需要扩展或面向对象设计时优先用
interface,在处理映射类型或联合类型时用type。 - 将泛型视为“类型函数”,把重复的类型逻辑抽象为可复用的工具。
- 善用内置工具类型,别自己重新定义
Partial或Pick,它们已经存在且经过真实项目验证。
类型系统不是负担,而是你在代码海洋中的导航仪。随着项目规模增长,良好的类型设计能让你在重构、协作、维护时充满自信。后续章节还会将类型安全落实到实际工程化流程中,让你体会它带来的长期收益。