TypeScript 基础类型系统
为什么类型系统是工程基础
TypeScript 的核心价值不在于"给 JavaScript 加类型",而在于通过静态分析将一大类运行时错误提前到编译阶段暴露。在大型前端项目中,类型系统充当着可执行的文档和重构安全网的双重角色。
面试定位:基础类型系统是 TypeScript 面试的入门门槛。面试官通过 interface vs type、联合类型的收窄方式等问题,快速判断候选人是否真正理解类型系统,还是仅停留在 any 和类型断言的层面。
原始类型与类型标注
JavaScript 原始类型在 TypeScript 中的映射
TypeScript 为 JavaScript 的七种原始类型提供一对一映射:
const name: string = "Alice";
const age: number = 30;
const active: boolean = true;
const id: bigint = 9007199254740991n;
const key: symbol = Symbol("key");
const empty: null = null;
const missing: undefined = undefined;
工程要点:
- 始终使用小写形式(
string而非String)。大写形式指向的是 JavaScript 包装对象,几乎没有正当的使用场景。 null和undefined在开启strictNullChecks后成为独立类型,不再隐式赋值给其他类型。这是 TypeScript 类型安全的基石之一。
数组与元组
// 数组:两种等价写法
const scores: number[] = [90, 85, 92];
const names: Array<string> = ["Alice", "Bob"];
// 元组:固定长度和位置类型
const pair: [string, number] = ["age", 30];
const rgb: [number, number, number] = [255, 128, 0];
// 带标签的元组(TypeScript 4.0+),提升可读性
type Point = [x: number, y: number, z: number];
any / unknown / never / void
这四个特殊类型构成了 TypeScript 类型系统的边界:
| 类型 | 语义 | 赋值方向 | 适用场景 |
|---|---|---|---|
any |
放弃类型检查 | 可赋值给任何类型,也可接受任何值 | 渐进迁移遗留代码 |
unknown |
类型安全的顶层类型 | 可接受任何值,但使用前必须收窄 | 处理外部输入 |
never |
不可能存在的值 | 是所有类型的子类型,没有值可以赋给它 | exhaustive check、抛出异常 |
void |
无返回值 | 仅用于函数返回 | 副作用函数 |
// unknown 强制收窄
function processInput(input: unknown): string {
if (typeof input === "string") return input.toUpperCase();
if (typeof input === "number") return input.toFixed(2);
throw new Error("Unsupported input type");
}
// never 用于 exhaustive check
type Shape = "circle" | "square";
function getArea(shape: Shape): number {
switch (shape) {
case "circle": return Math.PI * 10 ** 2;
case "square": return 10 ** 2;
default:
const _exhaustive: never = shape;
return _exhaustive;
}
}
Interface 与 Type Alias
核心区别
两者在大多数场景下可以互换,但存在关键差异:
// Interface:声明合并(Declaration Merging)
interface User {
name: string;
}
interface User {
age: number;
}
// User 自动合并为 { name: string; age: number }
// Type Alias:不支持声明合并,但支持更多类型运算
type Result = Success | Failure;
type Coords = [number, number];
type Callback = (event: Event) => void;
选择原则:
| 场景 | 推荐 | 原因 |
|---|---|---|
| 对象形状定义 | interface |
支持 extends 继承、声明合并,更适合面向对象的 API 设计 |
| 联合类型、交叉类型、元组 | type |
interface 无法表达这些类型运算 |
| 第三方库的类型扩展 | interface |
声明合并能力使用户无需修改源码即可扩展类型 |
| 函数类型 | 两者皆可 | type 的写法通常更简洁 |
extends vs 交叉类型
// Interface 继承
interface Animal { name: string }
interface Dog extends Animal { breed: string }
// Type 交叉
type Animal = { name: string };
type Dog = Animal & { breed: string };
两者在大多数情况下等价,但在类型冲突时行为不同:extends 会报错,而 & 会产生 never。
联合类型与交叉类型
联合类型(Union Types)
联合类型表示"A 或 B"的关系,是 TypeScript 中最常用的类型组合方式:
type Status = "idle" | "loading" | "success" | "error";
type ID = string | number;
function formatId(id: ID): string {
// 使用前必须收窄
if (typeof id === "string") return id.toUpperCase();
return id.toString();
}
判别联合类型(Discriminated Unions)
这是 TypeScript 中最强大的模式之一,通过共享的字面量属性实现类型安全的分支处理:
type ApiResponse =
| { status: "success"; data: unknown }
| { status: "error"; message: string }
| { status: "loading" };
function handleResponse(res: ApiResponse) {
switch (res.status) {
case "success":
console.log(res.data); // TypeScript 知道 data 存在
break;
case "error":
console.error(res.message); // TypeScript 知道 message 存在
break;
case "loading":
break;
}
}
交叉类型(Intersection Types)
交叉类型表示"A 且 B"的关系,用于组合多个类型:
type Timestamped = { createdAt: Date; updatedAt: Date };
type SoftDeletable = { deletedAt: Date | null };
type BaseEntity = Timestamped & SoftDeletable;
// { createdAt: Date; updatedAt: Date; deletedAt: Date | null }
字面量类型与 const 断言
字面量类型
TypeScript 允许将具体的值作为类型使用,这是联合类型威力的基础:
type Direction = "north" | "south" | "east" | "west";
type HttpMethod = "GET" | "POST" | "PUT" | "DELETE";
type DiceRoll = 1 | 2 | 3 | 4 | 5 | 6;
as const 断言
as const 将值的类型收窄到最精确的字面量形式:
// 没有 as const
const config = { endpoint: "/api", method: "GET" };
// 类型:{ endpoint: string; method: string }
// 使用 as const
const config = { endpoint: "/api", method: "GET" } as const;
// 类型:{ readonly endpoint: "/api"; readonly method: "GET" }
// 常见模式:从对象推导联合类型
const ROLES = ["admin", "editor", "viewer"] as const;
type Role = (typeof ROLES)[number]; // "admin" | "editor" | "viewer"
枚举(Enum)与替代方案
数值枚举与字符串枚举
// 字符串枚举
enum Direction {
North = "NORTH",
South = "SOUTH",
}
// 数值枚举(不推荐:容易出现反向映射陷阱)
enum Status {
Active, // 0
Inactive, // 1
}
为什么现代项目倾向避免 enum
- Tree-shaking 问题:数值枚举编译为 IIFE,bundler 无法消除未使用的成员
- 运行时开销:
const enum虽然内联,但在isolatedModules模式(Vite、esbuild)下行为不一致 - 联合类型替代方案更轻量:
// 推荐:联合类型 + as const 对象
const Direction = {
North: "NORTH",
South: "SOUTH",
} as const;
type Direction = (typeof Direction)[keyof typeof Direction];
// "NORTH" | "SOUTH"
类型断言与类型守卫
类型断言
类型断言是告诉编译器"我比你更了解这个值的类型":
const canvas = document.getElementById("main") as HTMLCanvasElement;
const ctx = canvas.getContext("2d")!; // 非空断言
工程纪律:类型断言绕过了类型检查,应视为技术债务。每个 as 都应有注释说明理由,团队应通过 lint 规则限制其使用频率。
基础类型守卫
function isString(value: unknown): value is string {
return typeof value === "string";
}
// 使用
function process(input: unknown) {
if (isString(input)) {
console.log(input.toUpperCase()); // TypeScript 知道是 string
}
}
函数类型
参数与返回值标注
// 函数声明
function greet(name: string, age?: number): string {
return age ? `${name}, ${age}` : name;
}
// 函数类型表达式
type Comparator<T> = (a: T, b: T) => number;
// 可调用接口
interface StringParser {
(input: string): number;
name: string;
}
函数重载
function createElement(tag: "div"): HTMLDivElement;
function createElement(tag: "span"): HTMLSpanElement;
function createElement(tag: string): HTMLElement;
function createElement(tag: string): HTMLElement {
return document.createElement(tag);
}
面试高频问题
Q: interface 和 type 有什么区别?如何选择?
回答要点:两者在对象类型定义上几乎等价。核心区别在于 interface 支持声明合并(适合库的类型扩展)和 extends 继承链,而 type alias 支持联合、交叉、元组等类型运算。在团队项目中,通常约定对象形状用 interface、类型运算用 type,保持一致性比选择本身更重要。
Q: any 和 unknown 的区别是什么?
回答要点:any 完全放弃类型检查,值可以任意使用而不报错。unknown 是类型安全的顶层类型——可以接受任何值,但使用前必须通过类型守卫或断言收窄。在处理外部数据(API 响应、JSON 解析)时,unknown 是正确选择,它迫使开发者显式处理类型不确定性。
Q: 什么是判别联合类型?有什么实际用途?
回答要点:判别联合类型是一组共享某个字面量属性(判别属性)的对象类型的联合。TypeScript 通过检查判别属性的值来收窄类型。这在处理 API 响应、状态机、Redux action 等场景中非常实用——它让编译器帮你确保每个分支都正确处理了所有可能的情况。