TS类型系统基础
# 01.TS类型系统基础
从 JavaScript 的自由王国到 TypeScript 的类型约束——理解 TS 类型系统的设计哲学与基础类型体系。
# 1. 案例引入
# 1.1 一段看似无害的 JS 代码
// 一个简单的用户服务函数
function formatUser(user) {
return `${user.name} - ${user.age}岁`;
}
// 某天同事这样调用:
formatUser({ name: "张三", age: "25" }); // "张三 - 25岁" ✅ 碰巧对
formatUser({ name: "李四" }); // "李四 - undefined岁" ❌ Bug A
formatUser(null); // ❌ TypeError: Cannot read properties of null
formatUser({ userName: "王五", userAge: 30 }); // "undefined - undefined岁" ❌ Bug C
线上事故:后端 API 字段从 age: number 改为 age: string,前端没有报错,但在计算总年龄时 "25" + "30" = "2530",导致整个报表数据异常——排查了 3 小时。
# 1.2 TypeScript 的答案
function formatUser(user: { name: string; age: number }): string {
return `${user.name} - ${user.age}岁`;
}
formatUser({ name: "张三", age: "25" }); // ❌ 编译错误!age 应为 number
formatUser({ name: "李四" }); // ❌ 编译错误!缺少 age
formatUser(null); // ❌ 编译错误!null 不能赋给 object
TS 的核心价值:将运行时错误提前到编译时。本文带你理解 TS 类型系统的基石——从每种基础类型的设计意图到实战应用。
# 2. 类型系统概览
# 2.1 JS 动态类型 vs TS 静态类型的本质区别
JavaScript(动态类型):
变量 → 运行时绑定类型 → 可以随时改变
let x = 1;
x = "hello"; // ✅ 完全合法,但危险的灵活性
TypeScript(静态类型):
变量 → 编译时确定类型 → 类型不可变
let x: number = 1;
x = "hello"; // ❌ 编译错误!保护代码质量
# 2.2 TS 类型层级金字塔
unknown (顶级类型——接收一切)
↗
any ──── unknown (any 放弃检查,unknown 保留安全)
↘
┌─────────┬─────────┐
│ object │ 基本类型 │
│ Array │ number │
│ tuple │ string │
│ { } │ boolean │
│ │ symbol │
│ │ bigint │
└────┬────┴────┬─────┘
│ │
├── null ─┤
│ undefined
│
never (底部类型——绝不可能)
疑惑:
any和unknown都能接收任意值,有什么区别? 结论:any关闭了类型检查——可以调用任意方法、访问任意属性;unknown保留了安全性——必须先收窄类型才能使用。后文详解。
# 3. 七种基本类型
# 3.1 number:不止是"数字"
let decimal: number = 42;
let hex: number = 0x2a; // 十六进制
let binary: number = 0b101010; // 二进制
let octal: number = 0o52; // 八进制
let big: number = 1_000_000; // 大数分隔符(ES2021)
// TS 的 number 涵盖整数 + 浮点数,都是 64 位双精度 IEEE 754
// 与 JS 一致:0.1 + 0.2 !== 0.3 的问题依然存在
# 3.2 string:模板字面量类型的基础
let name: string = "张三";
let greeting: string = `你好, ${name}`;
// TS 独有的字面量类型(后文高级类型会详细展开)
const hello = "hello"; // 类型被推断为字面量 "hello",而非 string
# 3.3 boolean:不仅仅是 true/false
let isDone: boolean = false;
// 类型守卫中 boolean 是基础判断依据
// 但注意:TS 不会自动把 truthy/falsy 当作 boolean
let flag = 42;
// if (flag) { ... } // ✅ 运行时可以,但 flag 类型是 number,不是 boolean
# 3.4 null 与 undefined:两位特殊的"空"
// 默认 strictNullChecks: false → null/undefined 可赋给任何类型(不安全)
// 默认 strictNullChecks: true → null/undefined 有自己的类型,不能赋给其他类型
let u: undefined = undefined;
let n: null = null;
// 严格模式下的"可空类型"
let nameOrNull: string | null = null; // 显式联合 null
let ageOrUndef: number | undefined = undefined;
// 类型守卫检查后才能安全使用
function greet(name: string | null) {
if (name === null) {
return "匿名用户";
}
return `你好, ${name}`; // name 被收窄为 string
}
# 3.5 symbol:唯一标识符
const sym1 = Symbol("id");
const sym2 = Symbol("id");
console.log(sym1 === sym2); // false——永远唯一
// 作为对象属性键,避免属性冲突
const user = {
[Symbol("id")]: 1,
name: "张三"
};
// unique symbol:字面量级别的 symbol 类型
const UniqueKey: unique symbol = Symbol("myKey");
// UniqueKey 的类型不是 symbol,而是 typeof UniqueKey(一个字面量类型)
# 3.6 bigint:处理超大整数
// bigint 和 number 不兼容,不能混用
let big: bigint = 9007199254740991n;
let num: number = 42;
// big + num; // ❌ bigint 不能与 number 运算
big + BigInt(num); // ✅ 显式转换
# 4. 数组与元组
# 4.1 数组的两种声明方式
// 语法 1:T[]
let numbers: number[] = [1, 2, 3];
let strings: string[] = ["a", "b", "c"];
// 语法 2:Array<T>(泛型写法,第 04 篇详解)
let numbers2: Array<number> = [1, 2, 3];
// 语法 1 更简洁,且对只读数组支持更好
let readonlyNumbers: readonly number[] = [1, 2, 3];
// readonlyNumbers.push(4); // ❌ 只读
// 二维数组
let matrix: number[][] = [
[1, 2],
[3, 4]
];
# 4.2 元组(Tuple):定长定类型的数组
// 元组的长度和每个位置类型都是固定的
let user: [string, number] = ["张三", 25];
user[0]; // string
user[1]; // number
// 超过长度赋值会报错(严格模式)
// user[2] = true; // ❌ 长度超出
// 元组的核心价值:函数返回多值
function getUser(): [string, number] {
return ["张三", 25];
}
const [name, age] = getUser(); // 解构时类型自动推断
# 4.3 元组越界与可选元素
// 带可选元素的元组
let opt: [string, number?] = ["hello"];
opt = ["hello", 42];
// 带 rest 元素的元组
let rest: [string, ...number[]] = ["hello", 1, 2, 3];
// 具名元组(可读性更好,但只是标记作用)
let named: [name: string, age: number] = ["张三", 25];
# 5. any、unknown、void、never 四兄弟
# 5.1 any:逃离类型检查
let danger: any = 4;
danger = "hello"; // ✅
danger.foo.bar(); // ✅ 编译通过,运行时炸!
danger();
// any 的"传染性"
let safe: number = danger; // ✅ any 污染了 safe 的类型
// 合法的使用场景:
// 1. JS→TS 迁移时的临时方案
// 2. 确实无法确定类型的第三方库返回值
// 3. 控制台调试
# 5.2 unknown:安全版本的 any
let unknown: unknown = 4;
unknown = "hello"; // ✅ 可以收任何值
// 区别来了:不能直接使用 unknown
// unknown.foo; // ❌ 对象类型为 unknown
// unknown(); // ❌ 不能调用
// 必须先类型收窄
if (typeof unknown === "string") {
console.log(unknown.toUpperCase()); // ✅ 收窄为 string
}
// unknown 是安全的"万能类型"——必须验证后才能使用
# 5.3 void:没有返回值
function log(msg: string): void {
console.log(msg);
// 没有 return 语句,或 return undefined
}
// void 主要用于回调函数的返回值位置
type Callback = () => void;
let cb: Callback = () => {
return 42; // 赋值给 void 类型时,返回值被忽略,不会报错
};
# 5.4 never:永不存在的类型
// never 表示"永远不会发生"的值
// 场景 1:总是抛出异常的函数
function throwError(): never {
throw new Error("出错了");
}
// 场景 2:死循环
function infiniteLoop(): never {
while (true) {}
}
// 场景 3:穷尽性检查(第 06 篇详述)
type Shape = "circle" | "square";
function area(shape: Shape): number {
switch (shape) {
case "circle": return Math.PI;
case "square": return 4;
default:
const exhausted: never = shape; // shape 应为 never
return exhausted;
}
}
四兄弟对比表:
| 类型 | 赋值给其他类型 | 可以调用方法 | 适用场景 |
|---|---|---|---|
any | 可以(污染一切) | 可以 | 临时迁移/放弃检查 |
unknown | 不可以(安全) | 不可以 | 接受未知值但需要验证 |
void | — | — | 函数无返回值 |
never | 可以赋给任何类型 | — | 不可能出现的分支 |
# 6. 枚举(Enum)
# 6.1 数字枚举
enum Direction {
Up, // 0
Down, // 1
Left, // 2
Right // 3
}
// 反向映射(数字枚举特有)
console.log(Direction.Up); // 0
console.log(Direction[0]); // "Up"
// 自定义起始值
enum Status {
Success = 200,
NotFound = 404,
ServerError = 500
}
# 6.2 字符串枚举
enum Color {
Red = "#FF0000",
Green = "#00FF00",
Blue = "#0000FF"
}
// 没有反向映射
console.log(Color.Red); // "#FF0000"
// console.log(Color["#FF0000"]); // ❌ 不存在
# 6.3 常量枚举(const enum)
const enum Size {
S = 1,
M = 2,
L = 3
}
let size = Size.M; // 编译后直接内联为 let size = 2; 不会有 Size 对象
// 适用:频繁使用且代码体积敏感的场景
// 注意:const enum 不能用于 .d.ts 声明文件中给外部使用
# 6.4 枚举 vs 联合类型:选型建议
// 枚举方案
enum HttpMethod {
GET = "GET",
POST = "POST",
DELETE = "DELETE"
}
function request(method: HttpMethod) {}
request(HttpMethod.GET); // 引入额外对象
// 字面量联合类型方案(更轻量)
type HttpMethod2 = "GET" | "POST" | "DELETE";
function request2(method: HttpMethod2) {}
request2("GET"); // 直接使用字符串,零运行时开销
// 选型原则:
// 有反向映射需求、需要语义化常量 → enum
// 仅需要类型约束、零开销 → 字面量联合类型
# 7. 字面量类型
# 7.1 字符串字面量类型
// 不仅约束类型为 string,还约束具体值
const hello = "Hello"; // 类型: "Hello"(不是 string)
let str: "foo" = "foo";
// str = "bar"; // ❌
// 与联合类型配合——替代枚举的轻量方案
type Alignment = "left" | "center" | "right";
function align(direction: Alignment) {}
align("left"); // ✅
// align("top"); // ❌
# 7.2 数字字面量类型与布尔字面量类型
type Dice = 1 | 2 | 3 | 4 | 5 | 6;
let roll: Dice = 3;
// roll = 7; // ❌
type Truthy = true;
let flag: Truthy = true;
// flag = false; // ❌
# 7.3 const 断言(as const)
// 普通声明 → 宽泛类型
let config1 = { host: "localhost", port: 3000 };
// config1.host: string, config1.port: number
// as const → 最窄类型(字面量 + readonly)
let config2 = { host: "localhost", port: 3000 } as const;
// config2.host: "localhost", config2.port: 3000
// config2.host = "other"; // ❌ readonly
// 数组 → readonly tuple
let colors = ["red", "green", "blue"] as const;
// colors: readonly ["red", "green", "blue"]
# 8. 联合类型与交叉类型
# 8.1 联合类型(Union):或
// A | B 表示值可以是 A 类型或者 B 类型
type ID = string | number;
function printID(id: ID) {
// id.length; // ❌ 只有 string 有 length
if (typeof id === "string") {
console.log(id.length); // ✅ 收窄为 string
} else {
console.log(id); // id 被收窄为 number
}
}
// 联合类型是 TS 处理 JS 动态性的核心武器
type Result = { success: true; data: object } | { success: false; error: string };
function handle(result: Result) {
if (result.success) {
console.log(result.data);
} else {
console.log(result.error);
}
}
# 8.2 交叉类型(Intersection):且
// A & B 表示值必须同时满足 A 和 B
type HasName = { name: string };
type HasAge = { age: number };
type Person = HasName & HasAge;
// { name: string; age: number }
// 与 interface extends 的区别:交叉类型可以合并基本类型
// 但 number & string 的结果是 never——不存在同时是两者的值
# 8.3 联合与交叉的组合使用
// 接口的交叉合并
interface Colorful { color: string; }
interface Circle { radius: number; }
type ColorfulCircle = Colorful & Circle;
const cc: ColorfulCircle = { color: "red", radius: 10 };
// 交叉 + 联合的关键用法
type Union = { a: string } | { b: number }; // 可以是 {a} 或 {b}
type Intersection = { a: string } & { b: number }; // 必须同时有 {a, b}
# 9. 类型推断与类型断言
# 9.1 类型推断:TS 自动推导
// 大部分情况下不需要显式标注类型
let x = 42; // number
let arr = [1, 2, 3]; // number[]
let obj = { a: 1 }; // { a: number }
// 最佳类型通用类型
let mixed = [1, "hello"]; // (string | number)[]
// 上下文推断
window.onmousedown = function (e) {
console.log(e.button); // e 被推断为 MouseEvent——基于上下文
};
# 9.2 类型断言(Type Assertions)
// 告诉编译器"我知道这个类型是什么"
// 语法 1:as Type
let value: unknown = "hello";
let len1 = (value as string).length;
// 语法 2:<Type>(不推荐,与 JSX 冲突)
let len2 = (<string>value).length;
// 双重断言(极度危险,仅用于极端情况)
// let bad = (value as unknown as number); // 绕过类型检查
// 适当的使用场景:DOM 元素获取
const canvas = document.getElementById("myCanvas") as HTMLCanvasElement;
const ctx = canvas.getContext("2d");
// 此处 canvas 不会是 null——开发者已知 DOM 存在
# 9.3 非空断言(!)
function getValue(): string | undefined {
return Math.random() > 0.5 ? "hello" : undefined;
}
let v = getValue();
// console.log(v.length); // ❌ v 可能为 undefined
console.log(v!.length); // ✅ 你告诉 TS:v 一定不是 null/undefined
// 使用前提:你 100% 确定值不为空,否则运行时依然崩
# 10. 速查表
| 类型 | 示例 | 关键特性 |
|---|---|---|
number | 42, 0x2a, 0b101010 | 64 位浮点,含整数和浮点 |
string | "hello", \${x}`` | 模板字面量类型的基础 |
boolean | true, false | 不自动含 truthy/falsy |
null | null | strictNullChecks 下独立类型 |
undefined | undefined | 同上 |
symbol | Symbol("id") | unique symbol 是字面量 |
bigint | 9007199254740991n | 不能与 number 混用 |
T[] | number[] | 数组 |
[string, number] | ["A", 1] | 元组:定长定类型 |
any | — | 逃离 TS,传染性 |
unknown | — | 安全的 top type |
void | — | 函数无返回值 |
never | — | 永不存在的类型 |
"left" \| "right" | — | 字面量联合 |
A & B | — | 交叉:同时满足 |
A \| B | — | 联合:满足其一 |
as const | — | 最窄推断 + readonly |
下一篇:02.TS接口与对象类型
上次更新: 2026/06/24, 12:59:24