主题
第 09 章 · TypeScript 基础类型
一句话开篇:TypeScript 给 JavaScript 装上了"安检门"——在你打包发车之前,提前帮你检查好每一件行李是不是装错了箱子。
0. 生活类比:从"自助餐"到"分餐制"
想象你开了一家餐厅。
JavaScript 就像自助餐:客人随便拿盘子,你也不知道他端走的是不是甜品(说不定他把咸鱼放进了草莓酱里)。等到客人吐了,你才知道出问题了——这就是运行时报错。
TypeScript 就像分餐制:每一道菜都有自己的盘子、自己的位置、自己的标签:
- 这盘是牛排(
steak: Steak),不能放进甜品区 - 这碗是汤(
soup: Soup),不能用筷子夹 - 这杯是红酒或白酒(
wine: Red | White),二选一
服务员(TypeScript 编译器)在上菜之前,就把所有错乱拦在厨房——客人吃到嘴里之前,错误就已经被发现了。
一句话:JS 是"先跑起来再说",TS 是"先检查再跑"。
1. 概念是什么
TypeScript(TS) 是微软在 2012 年推出的开源编程语言,它是 JavaScript 的超集(Superset):
┌──────────────────────────────────┐
│ TypeScript │
│ ┌──────────────────────────┐ │
│ │ JavaScript │ │
│ │ (所有 JS 都是合法 TS) │ │
│ └──────────────────────────┘ │
│ + 静态类型系统 │
│ + 接口、泛型、枚举 │
│ + 编译时检查 │
└──────────────────────────────────┘
│ tsc 编译
▼
┌──────────────────────────────────┐
│ JavaScript │
│ (浏览器/Node 真正执行) │
└──────────────────────────────────┘核心特性:
- 静态类型:变量、参数、返回值都可以声明类型
- 编译时检查:写错类型在 IDE 里就标红,不用等到上线
- 类型擦除:编译后产物是纯 JS,运行时没有任何额外开销
- 渐进式:可以一点一点加类型,新老代码无缝共存
2. 为什么需要 TS
真实业务场景
假设你正在写一个电商订单的函数:
js
// JavaScript 版本
function calcPrice(order) {
return order.price * order.count + order.tax;
}
calcPrice({ price: 10, count: "2", tax: 1 }); // "10221" 哦豁,字符串拼接了
calcPrice({ price: 10, qty: 2, tax: 1 }); // NaN 字段写错了
calcPrice(null); // 💥 TypeError这三种 bug 全部要等到用户下单的时候才发现——客服电话已经被打爆了。
ts
// TypeScript 版本
interface Order {
price: number;
count: number;
tax: number;
}
function calcPrice(order: Order): number {
return order.price * order.count + order.tax;
}
calcPrice({ price: 10, count: "2", tax: 1 }); // ❌ 编译报错
calcPrice({ price: 10, qty: 2, tax: 1 }); // ❌ 编译报错(缺 count)
calcPrice(null); // ❌ 编译报错写下错误代码的那一刻,编辑器就红线了——根本没机会上线。
JS vs TS 一表对比
| 维度 | JavaScript | TypeScript |
|---|---|---|
| 类型系统 | 动态(运行时) | 静态(编译时) |
| 报错时机 | 用户触发后 | 写代码时 |
| IDE 体验 | 自动补全靠猜 | 精准提示、跳转、重构 |
| 重构成本 | 改一个改全炸 | 编译器先帮你找出所有连带处 |
| 学习曲线 | 低 | 中(多学一套类型语法) |
| 运行时开销 | 无 | 无(编译后就是 JS) |
| 团队协作 | 看注释/猜接口 | 类型即文档 |
| 适合 | 小脚本、原型 | 中大型项目、长期维护 |
3. 怎么用(最小示例)
一行入门
ts
const name: string = "Alice";
const age: number = 18;
const isVip: boolean = true;
function greet(user: string): string {
return `Hello, ${user}!`;
}
console.log(greet(name));在浏览器中尝试
最简单的方式是打开 TypeScript Playground,左边写 TS、右边自动看到编译后的 JS。
本地编译
bash
npm i -g typescript
tsc hello.ts # 生成 hello.js
node hello.js4. 进阶要点
4.1 基本类型一览
| 类型 | 含义 | 示例 |
|---|---|---|
string | 字符串 | "abc" |
number | 数字(整数/浮点都算) | 42、3.14 |
boolean | 真假 | true / false |
null | 空值 | null |
undefined | 未定义 | undefined |
symbol | 唯一标识 | Symbol("id") |
bigint | 大整数 | 9007199254740993n |
any | "我放弃了",任何值都行 | let x: any = 1; x = "abc"; |
unknown | "暂时不知道",更安全的 any | 必须类型守卫后才能用 |
void | 函数没有返回值 | function log(): void {} |
never | 永远不会返回(抛异常/死循环) | function fail(): never { throw ... } |
any vs unknown vs never 的关键区别
ts
let a: any = 1;
a.foo.bar.baz(); // ✅ 编译通过(运行时直接炸)
let u: unknown = 1;
u.toFixed(); // ❌ 编译报错:必须先收窄类型
if (typeof u === "number") {
u.toFixed(); // ✅ 这里 u 被收窄为 number
}
function loop(): never {
while (true) {} // 永远不返回
}类比:
any:信任客户,他说啥我都信unknown:陌生人来店,得先验明正身才能服务never:不可能发生,比如"下雪天太阳从西边出"
4.2 数组与元组
ts
// 数组:所有元素同类型
const nums: number[] = [1, 2, 3];
const strs: Array<string> = ["a", "b"]; // 等价写法
// 元组:固定长度、每个位置类型不同
const point: [number, number] = [10, 20];
const user: [string, number, boolean] = ["Alice", 18, true];
// 越界 / 类型错都会报错
point[2]; // ❌ 长度只有 2
user[1] = "abc"; // ❌ 第 2 个必须是 number类比:
- 数组 = "一筐苹果"(同种水果,数量随意)
- 元组 = "宜家收纳盒"(格子固定,第 1 格放螺丝、第 2 格放螺母)
4.3 对象类型与字面量类型
ts
const user: { name: string; age: number } = { name: "A", age: 18 };
// 字面量类型:值本身就是类型
let dir: "up" | "down" | "left" | "right";
dir = "up"; // ✅
dir = "north"; // ❌字面量类型 + 联合 = TS 版的枚举,强大且零运行时开销。
4.4 接口(interface) vs 类型别名(type)
| 维度 | interface | type |
|---|---|---|
| 描述对象 | ✅ 强项 | ✅ 可以 |
| 描述联合/交叉/原始类型 | ❌ | ✅ |
| 同名合并(声明合并) | ✅ 自动合并 | ❌ 报错 |
| 继承 | extends | & 交叉 |
| 推荐场景 | 公开 API、类的契约 | 复杂组合、工具类型 |
ts
// interface
interface User {
name: string;
}
interface User {
age: number; // 同名自动合并,最终 User = { name; age }
}
// type
type ID = string | number; // interface 做不到
type Admin = User & { role: "admin" }; // 交叉
// 经验法则
// 描述"对象/类的形状" → 优先 interface
// 描述"联合/交叉/工具类型" → 用 type4.5 联合 | 与交叉 &
ts
type StringOrNumber = string | number; // 联合:是其中一个
type FullUser = User & Admin; // 交叉:两者都是
function format(x: string | number) {
// 这里 x 还不能直接调用 .toUpperCase(),需要类型守卫
if (typeof x === "string") return x.toUpperCase();
return x.toFixed(2);
}类比:
- 联合
|= "套餐 A 或 套餐 B"(二选一) - 交叉
&= "VIP 卡 + 学生证"(两个身份合一)
4.6 函数类型
ts
// 完整签名
function add(a: number, b: number): number {
return a + b;
}
// 可选参数 ?
function greet(name: string, title?: string) {
return `${title ?? ""} ${name}`;
}
// 默认参数
function pow(x: number, n: number = 2) {
return x ** n;
}
// 剩余参数
function sum(...nums: number[]): number {
return nums.reduce((a, b) => a + b, 0);
}
// 函数类型别名
type Comparator<T> = (a: T, b: T) => number;
const byAge: Comparator<{ age: number }> = (a, b) => a.age - b.age;4.7 类型断言(as)
"我比编译器更清楚这是什么"——用之前请三思。
ts
const el = document.getElementById("app") as HTMLDivElement;
el.innerText = "Hi";
// 双重断言(仅在迫不得已时使用)
const x = "123" as unknown as number;类比:类型断言 = 跟安检员说"放心,这真的是水"——错了,自己负责。
4.8 类型守卫(Type Guard)
让 TS 在分支里"自动收窄"类型。
ts
// 1. typeof 守卫(仅原始类型)
function pad(x: string | number) {
if (typeof x === "string") {
return x.padStart(3, "0"); // x 是 string
}
return x.toFixed(2); // x 是 number
}
// 2. instanceof 守卫(仅类)
function logErr(e: Error | string) {
if (e instanceof Error) console.log(e.stack);
else console.log(e);
}
// 3. in 守卫(对象属性)
type Cat = { meow: () => void };
type Dog = { bark: () => void };
function speak(p: Cat | Dog) {
if ("meow" in p) p.meow();
else p.bark();
}
// 4. 自定义守卫(is 关键字)
function isCat(p: Cat | Dog): p is Cat {
return (p as Cat).meow !== undefined;
}ASCII 图——类型收窄过程:
输入:x: string | number
│
┌────────┴─────────┐
│ typeof x === "string"
│ │ │
▼ Yes │ ▼ No
x: string │ x: number
│ │ │
.padStart│ .toFixed5. 实战案例
详见 examples/:
| 文件 | 内容 |
|---|---|
basic-types.ts | 11 种基础类型完整示例 |
interface-vs-type.ts | 接口 vs 类型别名 5 大对比 |
union-intersection.ts | 联合/交叉的真实业务用法 |
type-guard.ts | 4 种类型守卫的写法 |
6. ⚠️ 易踩的坑
- 滥用
any:写起来爽,等于关掉了 TS——团队里应该禁用any,改用unknown+ 守卫。 null与undefined不开严格模式:建议在tsconfig.json打开"strict": true,否则string也可能是null。- 类型断言当万能膏药:
as Foo不会做任何运行时检查,断言错了照样炸。 - 数组类型
Array<T>与T[]混用:本质等价,项目内统一一种风格即可。 {}不是"空对象":它表示"任何非 null/undefined 的值",连1、"abc"都算合法。空对象应写Record<string, never>或{ [k: string]: never }。Function类型:所有函数都符合,但不知道参数和返回值,应改用具体签名(x: number) => string。enum慎用:会生成额外运行时代码,且行为有歧义(数字枚举可双向映射),新代码推荐用as const+ 联合字面量替代。
ts
// ❌ 老派
enum Status { Open, Closed }
// ✅ 现代
const Status = { Open: "OPEN", Closed: "CLOSED" } as const;
type Status = typeof Status[keyof typeof Status];7. 一句话总结
TypeScript 不是"另一种语言",而是给 JavaScript 加了一层"翻译前的安检"——多花 10% 时间写类型,少花 90% 时间排错。
8. 延伸阅读
- 官方手册:TypeScript Handbook
- 在线试玩:TS Playground
- 推荐配置:tsconfig/bases
- 中文教程:TypeScript 入门教程