TypeScript类型系统核心概念复盘
如果你写过一段时间的JavaScript代码,一定遇到过类似这样的报错:`Cannot read property 'xxx' of undefined`,或者`undefined is not a function`。这些错误都源于JavaScript的动态类型特性:变量可以在运行时被赋予任意类型的值,类型错误只有在代码执行时才会暴露,轻则导致页面功能异常,重则引发线上生产事故。TypeScript(以下简称TS)的核心价值就是通过静态类型检查,在代码编译阶段就捕获这类类型错误,从根源上提升代码的可靠性和可维护性。
TS的类型系统建立在JavaScript的基础上,既保留了JS的灵活性,又通过类型约束降低了出错的概率。本章将系统复盘TS类型系统的核心基础概念,所有知识点都会结合真实开发场景的示例讲解,避免纯理论堆砌,帮你快速对齐前置知识,为后续学习泛型、条件类型、映射类型等进阶内容打下坚实基础。
---
1.1 原始类型:TS类型系统的基石
原始类型是TS中最基础的原子类型,对应JavaScript中的原始值类型。TS严格区分了原始类型和对象类型,所有原始类型的值都是不可变的,赋值时是值拷贝。
1.1.1 基础原始类型
TS支持所有JavaScript标准的原始类型,语法和JS几乎一致,只需要在变量声明时添加类型标注即可: ```typescript // 字符串类型 const userName: string = "张三"; const greeting: string = `你好,${userName}!`; // 支持模板字符串
// 数字类型(包含整数、浮点数、NaN、Infinity) const age: number = 25; const pi: number = 3.14159; const infinity: number = Infinity;
// 布尔类型 const isVip: boolean = true; const hasPermission: boolean = false;
// null类型:表示空值 const emptyValue: null = null;
// undefined类型:表示未定义 const notDefined: undefined = undefined;
// symbol类型:表示唯一的、不可变的标识符(ES6+) const userIdKey: symbol = Symbol("user_id"); const userMap: Record<symbol, string> = { [userIdKey]: "10001" };
// bigint类型:表示大整数(ES2020+) const bigNumber: bigint = 9007199254740991n; const bigSum: bigint = 9007199254740991n + 1n; ```
> 💡 新手常见坑点:在`strictNullChecks: false`(TS默认非严格模式)下,`null`和`undefined`可以被隐式赋值给任意类型,这是JS历史遗留的宽松设计。但生产环境强烈推荐开启严格模式,这种隐式赋值会被编译器拦截,避免空值导致的运行时错误。
1.1.2 特殊原始类型
除了标准原始类型,TS还定义了两种特殊的原始类型:`void`和`never`,用于描述特殊场景下的返回值类型。
- `void`:表示没有任何返回值的类型,通常用于标注没有返回值的函数:
```typescript // 函数没有return语句,或者return后面没有值,返回类型就是void function logMessage(msg: string): void { console.log(`[${new Date().toISOString()}] ${msg}`); } ``` 注意:`void`类型的变量只能被赋值为`undefined`(严格模式下),但函数返回`void`并不代表函数真的不能返回值,只是编译器会忽略返回值: ```typescript function testVoid(): void { return 123; // 编译不会报错,但返回值会被忽略 } const res = testVoid(); // res的类型是void,不能赋值给其他类型 ```
- `never`:表示永远不会有返回值的类型,通常用于标注永远会抛出错误、或者永远无法执行到返回语句的函数:
```typescript // 函数抛出错误,永远不会正常返回 function throwError(msg: string): never { throw new Error(msg); }
// 无限循环的函数,永远不会返回 function infiniteLoop(): never { while (true) { console.log("running..."); } }
// 联合类型中never的作用:当联合类型的最后一个成员是never时,表示没有其他可能的类型 type ImpossibleType = string & number; // 等价于never,因为不存在既是string又是number的值 ```
---
1.2 对象类型:描述复杂数据结构
JavaScript是面向对象的语言,大部分业务数据都是以对象的形式存在的,TS的对象类型就是用来描述这类复杂数据结构的。
1.2.1 字面量对象类型
最简单的对象类型定义方式是直接写对象的结构,每个属性标注对应的类型: ```typescript // 字面量对象类型:直接描述对象的结构 const user: { name: string; age: number; email?: string; // 可选属性:加?标记,表示这个属性可以不存在 readonly id: number; // 只读属性:赋值后不能修改 } = { name: "李四", age: 30, id: 1001 };
// 可选属性的使用场景:不是所有对象都必须有这个属性 user.email = "lisi@example.com"; // 正常赋值 delete user.email; // 也可以删除可选属性
// 只读属性的限制:赋值后无法修改 // user.id = 1002; // 编译报错:Cannot assign to 'id' because it is a read-only property. ```
如果对象的属性名是不确定的,可以使用索引签名来定义: ```typescript // 索引签名:定义任意字符串key对应的value类型 interface StringKeyMap { [key: string]: string; }
const config: StringKeyMap = { apiUrl: "https://api.example.com", appName: "我的应用", version: "1.0.0" };
// 也可以定义数字索引签名,通常用于类数组结构 interface NumberArray { [index: number]: string; } const arr: NumberArray = ["a", "b", "c"]; ```
> ⚠️ 注意:索引签名不能和具体属性冲突,比如`{ [key: string]: string; name: number }`会报错,因为`name`的number类型不符合索引签名的string要求。
1.2.2 接口与类型别名
当对象类型需要被复用的时候,我们可以用`interface`(接口)或`type`(类型别名)来定义可复用的类型模板。
接口(interface)
接口是TS中专门用于定义对象结构的语法,支持继承、声明合并等特性: ```typescript // 定义用户接口 interface User { id: number; name: string; age: number; email: string; }
// 接口继承:扩展已有接口的属性 interface AdminUser extends User { role: "admin"; permissions: string[]; loginIp?: string; // 扩展可选属性 }
// 声明合并:同一个接口可以多次声明,属性会自动合并 interface User { avatar: string; // 第二次声明User接口,新增avatar属性 }
// 最终User接口的结构是:{ id: number; name: string; age: number; email: string; avatar: string } const user: User = { id: 1, name: "王五", age: 28, email: "wangwu@example.com", avatar: "https://example.com/avatar.png" }; ```
类型别名(type)
类型别名可以给任意类型起一个别名,不仅能定义对象类型,还能定义联合类型、元组、函数类型等: ```typescript // 类型别名定义对象类型 type User = { id: number; name: string; age: number; email: string; };
// 类型别名定义联合类型 type ID = string | number;
// 类型别名定义函数类型 type Callback = (msg: string, code: number) => void;
// 类型别名扩展:用交叉类型合并多个类型 type AdminUser = User & { role: "admin"; permissions: string[]; }; ```
接口 vs 类型别名:怎么选?
很多初学者会纠结这两个语法选哪个,其实官方推荐的原则非常简单: 1. 定义对象结构优先用接口:接口支持声明合并,适合开发库、插件等需要被外部扩展的场景,错误提示也更友好。 2. 定义非对象类型(联合、交叉、工具类型等)用类型别名:类型别名的语法更灵活,能覆盖所有类型场景。
1.2.3 多余属性检查:TS的“拼写纠错”机制
当你给接口类型的对象字面量赋值时,TS会自动做多余属性检查:如果对象中有接口未定义的属性,会直接报错。这个机制的设计初衷是帮你发现拼写错误: ```typescript interface Product { id: number; name: string; price: number; }
const product: Product = { id: 1, name: "手机", price: 3999, cateogry: "数码" // 拼写错误,应该是category,TS直接报错 }; ```
如果确实需要传递接口未定义的额外属性,可以通过以下两种方式绕过检查: 1. 用类型断言标记为`any`: ```typescript const product: Product = { id: 1, name: "手机", price: 3999, category: "数码" } as any; // 绕过多余属性检查 ``` 2. 给接口添加索引签名,允许任意额外属性: ```typescript interface Product { id: number; name: string; price: number; [key: string]: any; // 允许任意额外属性 } ```
---
1.3 数组与元组:有序数据集合的两种形态
TS中的数组和元组都用于存储有序的数据集合,但适用场景完全不同。
1.3.1 数组类型
数组是相同类型元素的集合,TS中有两种语法定义数组类型: ```typescript // 语法1:类型后面加[],最常用 const fruits: string[] = ["苹果", "香蕉", "橙子"]; const numbers: number[] = [1, 2, 3, 4, 5];
// 语法2:泛型Array<类型>,功能完全一致 const fruits2: Array<string> = ["苹果", "香蕉", "橙子"];
// 任意类型数组(不推荐,等于放弃了类型检查) const anyArray: any[] = [1, "hello", true, { a: 1 }]; ```
如果需要定义不可变的数组,可以添加`readonly`修饰符,此时数组的所有修改方法(push、pop、splice等)都会被禁用: ```typescript const readonlyFruits: readonly string[] = ["苹果", "香蕉"]; // readonlyFruits.push("橙子"); // 编译报错:Property 'push' does not exist on type 'readonly string[]' ```
1.3.2 元组类型
元组是TS特有的类型,表示固定长度、固定类型的数组,适合描述结构固定的有序数据,比如函数返回值、API接口的固定响应结构等: ```typescript // 基础元组:固定2个元素,第一个是string,第二个是number const userTuple: [string, number] = ["张三", 25];
// 元组的解构使用 const [name, age] = userTuple; console.log(name); // "张三" console.log(age); // 25
// 带标签的元组(TS 4.0+支持):提升可读性,不用记忆每个位置的类型 type UserTuple = [name: string, age: number, isVip: boolean]; const user2: UserTuple = ["李四", 30, true]; const [userName, userAge, vip] = user2; // 变量名和标签对应,可读性更高 ```
可变元组(Rest元组)
TS支持用`...`语法定义可变长度的元组,非常适合处理函数参数、动态拼接数组等场景: ```typescript // 可变元组:第一个元素是object类型,后面是任意数量的Function类型 type ConfigCallback = [config: object, ...callbacks: Function[]];
function setup(...args: ConfigCallback) { const [config, ...cbs] = args; console.log("初始化配置:", config); cbs.forEach(cb => cb()); // 执行所有回调 }
// 调用示例 setup( { debug: true, apiUrl: "https://api.example.com" }, () => console.log("日志模块初始化完成"), () => console.log("路由模块初始化完成"), () => console.log("全局初始化完成") ); ```
---
1.4 联合类型与交叉类型:类型的“与”“或”关系
联合类型和交叉类型是TS中处理类型组合的核心语法,也是后续学习高级工具类型的基础。
1.4.1 联合类型:或关系
联合类型用`|`分隔多个类型,表示变量的值可以是其中任意一种类型,核心场景是处理一个变量可能有多种类型的情况: ```typescript // 联合类型示例:函数参数可以是字符串或数字 function printValue(value: string | number) { if (typeof value === "string") { // 类型收窄:经过typeof判断后,TS自动将value的类型缩小为string console.log("字符串长度:", value.length); console.log("大写形式:", value.toUpperCase()); } else { // 收窄为number类型 console.log("数字取整:", Math.floor(value)); console.log("保留两位小数:", value.toFixed(2)); } }
printValue("hello"); // 正常 printValue(123.456); // 正常 // printValue(true); // 编译报错:布尔类型不在联合类型范围内 ```
联合类型的常见场景还包括:API响应可能是成功数据或错误信息、表单字段可能是值或者空、DOM元素可能是多种类型等。
1.4.2
1.4.2 交叉类型:与关系
交叉类型用`&`符号连接多个类型,表示变量需要同时满足所有类型的约束,核心场景是合并多个类型的属性、扩展已有类型的能力。我们可以用一个生活化的例子理解:如果「会编程」是一个类型,「会设计」是另一个类型,那么「会编程 & 会设计」就是同时掌握两种技能的全栈工程师。
```typescript // 基础交叉类型:合并两个对象类型的属性 type CanCode = { languages: string[]; writeCode: () => void; };
type CanDesign = { tools: string[]; drawUI: () => void; };
// 全栈工程师类型:同时具备两种能力 type FullStackDeveloper = CanCode & CanDes