TypeScript 核心概念与类型系统
TypeScript 核心概念与类型系统
TypeScript 是 JavaScript 的超集(Superset),为 JavaScript 添加了静态类型系统,提高了代码的可靠性和可维护性。它由微软在 2012 年推出,如今已成为中大型前端项目的事实标准。
"超集"是什么意思? 意思是任何合法的 JavaScript 代码都是合法的 TypeScript 代码。你可以把 `.js` 直接改名为 `.ts`,它依然能跑。TypeScript 在此基础上叠加了一层"类型标注",这些标注只在开发和编译阶段存在,编译后会被完全擦除,产物就是普通的 JavaScript。
可以用一个类比理解:JavaScript 像是不带说明书的乐高零件,你得凭记忆知道每块怎么拼;TypeScript 则是给每块零件都贴上了标签和拼装说明,一旦你拼错,说明书(编译器)会立刻提醒你,而不是等成品散架了才发现。
为什么要用 TypeScript
静态类型带来的收益是实打实的。微软和一些大型开源项目的统计显示,接入 TypeScript 后,约 15% 的常见 bug(类型相关、拼写错误、空值访问等)能在编译期被直接拦截,根本进不到运行时。
| 维度 | 纯 JavaScript | TypeScript |
| --- | --- | --- |
| 错误发现时机 | 运行时(用户触发) | 编译期(写代码时) |
| IDE 智能提示 | 有限、靠猜 | 精准、可跳转 |
| 重构安全性 | 靠全局搜索、易漏 | 类型驱动、改错即报 |
| 自文档化 | 需额外注释 | 类型即文档 |
| 大型协作 | 接口靠口头约定 | 接口由类型强制 |
| 学习/接入成本 | 无 | 有一定门槛 |
代价是需要写类型标注、配置编译工具、学习类型系统。但对于超过几千行、需要多人协作或长期维护的项目,这个投入回报比极高。
类型系统
TypeScript 的类型系统是"结构化类型系统"(Structural Typing),也叫"鸭子类型"——只要结构长得一样,就认为是同一类型,不关心名字。
基本类型:
类型推断:
类型断言:
代码示例
基本类型示例
// 原始类型
let name: string = 'Alice';
let age: number = 25;
let isStudent: boolean = true;
let nothing: null = null;
let notDefined: undefined = undefined;
let unique: symbol = Symbol('unique');
let bigNumber: bigint = 100n;
// 对象类型
let person: object = { name: 'Alice', age: 25 };
let numbers: number[] = [1, 2, 3];
let strings: Array<string> = ['a', 'b', 'c'];
let fn: Function = () => {};
// 联合类型
let value: string | number = 'hello';
value = 42;
// 交叉类型
type Person = { name: string };
type Employee = { id: number };
type PersonEmployee = Person & Employee;
const personEmployee: PersonEmployee = {
name: 'Alice',
id: 1
};字面量类型、元组与枚举
除了宽泛的原始类型,TypeScript 还能把"具体的值"当作类型,这在建模有限状态时极其有用。
// 字面量类型:值本身即类型
let direction: 'up' | 'down' | 'left' | 'right';
direction = 'up'; // OK
// direction = 'top'; // 错误:不在允许集合内
// 元组:固定长度、每个位置类型明确的数组
let pair: [string, number] = ['age', 25];
let rgb: [number, number, number] = [255, 128, 0];
// React 的 useState 返回值就是元组:[state, setState]
// 枚举:一组命名常量
enum Status {
Pending, // 0
Active, // 1
Closed, // 2
}
let s: Status = Status.Active;
// 字符串枚举(更利于调试,值有意义)
enum Role {
Admin = 'ADMIN',
User = 'USER',
Guest = 'GUEST',
}
// 现代实践:常量对象 + as const 常被用来替代枚举
const LogLevel = {
Debug: 'debug',
Info: 'info',
Error: 'error',
} as const;
type LogLevel = typeof LogLevel[keyof typeof LogLevel];
// 'debug' | 'info' | 'error'any、unknown、never、void 辨析
这四个特殊类型是初学者最容易混淆的,也是面试高频考点。
// any:放弃类型检查,"逃生舱",应尽量避免
let a: any = 4;
a.foo.bar; // 不报错,但运行时可能崩溃
a = 'string'; // 随意赋值
// unknown:类型安全版的 any,用前必须收窄
let u: unknown = getValue();
// u.toFixed(2); // 错误:不能直接使用
if (typeof u === 'number') {
u.toFixed(2); // OK:收窄后才能用
}
// void:表示函数没有返回值
function logMessage(msg: string): void {
console.log(msg);
}
// never:永远不会有值(抛异常或死循环)
function throwError(msg: string): never {
throw new Error(msg);
}
// never 也用于穷尽性检查(见下文类型守卫)| 类型 | 含义 | 能否赋给它 | 能否从它取值 | 建议 |
| --- | --- | --- | --- | --- |
| any | 关闭检查 | 任何值 | 任意使用 | 尽量不用 |
| unknown | 未知但安全 | 任何值 | 收窄后才能用 | 优先用它替代 any |
| void | 无返回值 | undefined | 无意义 | 标注无返回函数 |
| never | 不可能有值 | 无 | 无 | 穷尽检查、异常函数 |
类型推断示例
// 基于初始化值
let userName = 'Alice'; // 推断为 string
let count = 25; // 推断为 number
let isActive = true; // 推断为 boolean
// const 会推断为更窄的字面量类型
const literal = 'hello'; // 推断为 'hello',而非 string
// 基于上下文(回调参数类型自动推断)
const numbers = [1, 2, 3];
numbers.forEach(num => {
console.log(num.toFixed(2)); // num 被推断为 number
});
// 函数返回值推断
function add(a: number, b: number) {
return a + b; // 返回类型推断为 number
}
// 显式类型注解(先声明后赋值时需要)
let total: number;
total = 10;类型断言示例
// 使用 as 关键字
let value: unknown = 'hello';
let length: number = (value as string).length;
// 使用尖括号语法(在 JSX 中不推荐)
let length2: number = (<string>value).length;
// 非空断言:告诉编译器"这里一定不是 null/undefined"
function printName(name: string | null) {
console.log(name!.toUpperCase()); // ! 断言 name 不为 null
}
// DOM 元素类型断言
const button = document.querySelector('button') as HTMLButtonElement;
button.addEventListener('click', () => {
console.log('Button clicked');
});
// const 断言:把整个对象/数组冻结为最窄的只读字面量类型
const config = {
endpoint: '/api',
retries: 3,
} as const;
// config 的类型为 { readonly endpoint: '/api'; readonly retries: 3 }断言的风险: 断言是"绕过"检查,不是"转换"。如果你断言错了,编译器信你,运行时就会崩。所以断言要慎用,优先用类型守卫收窄。
接口和类型别名
接口(interface):
类型别名(type):
| 特性 | interface | type |
| --- | --- | --- |
| 描述对象结构 | 擅长 | 可以 |
| 联合/交叉类型 | 不能直接表达联合 | 擅长 |
| 扩展方式 | extends | & 交叉 |
| 声明合并 | 支持 | 不支持 |
| 表达原始类型/元组 | 不能 | 可以 |
| 性能(大型项目) | 略优(缓存友好) | 复杂交叉略慢 |
经验法则:描述对象/类的公共契约用 interface,需要联合类型或复杂类型运算用 type。 两者能力大幅重叠,团队内保持一致即可。
代码示例
接口示例
// 基本接口
interface Person {
name: string;
age: number;
}
const person: Person = {
name: 'Alice',
age: 25
};
// 可选属性
interface Product {
id: number;
name: string;
price?: number; // 可选属性
}
const product: Product = {
id: 1,
name: 'Laptop'
};
// 只读属性
interface Point {
readonly x: number;
readonly y: number;
}
const point: Point = { x: 10, y: 20 };
// point.x = 15; // 错误:只读属性不能修改
// 索引签名:允许任意字符串键
interface StringMap {
[key: string]: string;
}
const headers: StringMap = { 'Content-Type': 'application/json' };
// 函数类型接口
interface SearchFn {
(source: string, keyword: string): boolean;
}
const search: SearchFn = (src, kw) => src.includes(kw);
// 接口扩展
interface Animal {
name: string;
}
interface Dog extends Animal {
breed: string;
}
const dog: Dog = {
name: 'Buddy',
breed: 'Golden Retriever'
};
// 接口实现
interface Clock {
currentTime: Date;
setTime(d: Date): void;
}
class DigitalClock implements Clock {
currentTime: Date = new Date();
setTime(d: Date) {
this.currentTime = d;
}
}
// 声明合并:同名接口自动合并(常用于扩展第三方类型)
interface Window {
myGlobalConfig: { debug: boolean };
}类型别名示例
// 基本类型别名
type Name = string;
type Age = number;
type Person = {
name: Name;
age: Age;
};
// 联合类型别名
type Status = 'pending' | 'success' | 'error';
type ID = string | number;
// 可辨识联合(Discriminated Union):最强大的建模工具
type Shape =
| { kind: 'circle'; radius: number }
| { kind: 'rectangle'; width: number; height: number }
| { kind: 'triangle'; base: number; height: number };
function area(shape: Shape): number {
switch (shape.kind) {
case 'circle':
return Math.PI * shape.radius ** 2;
case 'rectangle':
return shape.width * shape.height;
case 'triangle':
return (shape.base * shape.height) / 2;
}
}
// 函数类型别名
type Callback = (error: Error | null, data?: any) => void;
function fetchData(callback: Callback) {
// ...
}
// 泛型类型别名
type Container<T> = { value: T };
type NumberContainer = Container<number>;
// 条件类型别名
type NonNullableType<T> = T extends null | undefined ? never : T;
type Result = NonNullableType<string | null>; // string泛型
泛型(Generics)的概念:
打个比方:泛型就像函数的参数,只不过传的不是值,而是类型。`identity
泛型的使用场景:
泛型工具类型(内置):
代码示例
泛型函数示例
// 基本泛型函数
function identity<T>(arg: T): T {
return arg;
}
const num = identity<number>(42);
const str = identity('hello'); // 类型推断为 string
// 多个类型参数
function pair<T, U>(first: T, second: U): [T, U] {
return [first, second];
}
const pairResult = pair('hello', 42); // [string, number]
// 泛型约束:限制类型参数必须满足某种结构
interface Lengthwise {
length: number;
}
function getLength<T extends Lengthwise>(arg: T): number {
return arg.length;
}
getLength('hello'); // 5
getLength([1, 2, 3]); // 3
// getLength(42); // 错误:number 没有 length 属性
// 用 keyof 约束键,实现类型安全的属性访问
function getProp<T, K extends keyof T>(obj: T, key: K): T[K] {
return obj[key];
}
const user = { id: 1, name: 'Alice' };
const id = getProp(user, 'id'); // 类型为 number
const name = getProp(user, 'name'); // 类型为 string
// getProp(user, 'email'); // 错误:'email' 不存在
// 泛型默认值
function createArray<T = string>(length: number, value: T): T[] {
return Array(length).fill(value);
}泛型接口示例
// 泛型接口
interface Box<T> {
value: T;
}
const stringBox: Box<string> = { value: 'hello' };
const numberBox: Box<number> = { value: 42 };
// 泛型接口作为函数类型
interface Comparator<T> {
(a: T, b: T): number;
}
const numberComparator: Comparator<number> = (a, b) => a - b;
const stringComparator: Comparator<string> = (a, b) =>
a.localeCompare(b);
// 泛型接口作为类类型
interface Repository<T> {
findById(id: number): T | null;
save(entity: T): void;
}
interface User {
id: number;
name: string;
}
class UserRepository implements Repository<User> {
private users: User[] = [];
findById(id: number): User | null {
return this.users.find(u => u.id === id) || null;
}
save(user: User): void {
this.users.push(user);
}
}
// 泛型接口描述通用 API 响应
interface ApiResponse<T> {
code: number;
message: string;
data: T;
}
type UserResponse = ApiResponse<User>;
type UserListResponse = ApiResponse<User[]>;泛型类示例
// 泛型类
class Stack<T> {
private items: T[] = [];
push(item: T): void {
this.items.push(item);
}
pop(): T | undefined {
return this.items.pop();
}
peek(): T | undefined {
return this.items[this.items.length - 1];
}
get size(): number {
return this.items.length;
}
}
const numberStack = new Stack<number>();
numberStack.push(1);
numberStack.push(2);
console.log(numberStack.pop()); // 2
const stringStack = new Stack<string>();
stringStack.push('hello');
stringStack.push('world');
console.log(stringStack.pop()); // 'world'
// 泛型 + 约束的类:类型安全的键值缓存
class Cache<K extends string | number, V> {
private store = new Map<K, V>();
set(key: K, value: V): void {
this.store.set(key, value);
}
get(key: K): V | undefined {
return this.store.get(key);
}
}
const cache = new Cache<string, User>();
cache.set('u1', { id: 1, name: 'Alice' });泛型工具类型示例
interface User {
id: number;
name: string;
email: string;
age: number;
}
// Partial<T>:将所有属性变为可选
type PartialUser = Partial<User>;
const partialUser: PartialUser = {
name: 'Alice'
};
// 典型用途:更新函数只需传部分字段
function updateUser(id: number, changes: Partial<User>) {
// 只更新传入的字段
}
updateUser(1, { age: 26 });
// Required<T>:将所有属性变为必需
type RequiredUser = Required<Partial<User>>;
const requiredUser: RequiredUser = {
id: 1,
name: 'Alice',
email: 'alice@example.com',
age: 25
};
// Readonly<T>:将所有属性变为只读
type ReadonlyUser = Readonly<User>;
const readonlyUser: ReadonlyUser = {
id: 1,
name: 'Alice',
email: 'alice@example.com',
age: 25
};
// readonlyUser.name = 'Bob'; // 错误
// Pick<T, K>:拣选属性
type UserBasicInfo = Pick<User, 'id' | 'name'>;
const basicInfo: UserBasicInfo = {
id: 1,
name: 'Alice'
};
// Omit<T, K>:排除属性(如返回给前端时去掉敏感字段)
type PublicUser = Omit<User, 'email'>;
const publicUser: PublicUser = {
id: 1,
name: 'Alice',
age: 25
};
// Record<K, T>:创建键值对类型
type UserRecord = Record<number, User>;
const users: UserRecord = {
1: { id: 1, name: 'Alice', email: 'alice@example.com', age: 25 },
2: { id: 2, name: 'Bob', email: 'bob@example.com', age: 30 }
};
// 组合使用:常见的表单/DTO 建模
type CreateUserDTO = Omit<User, 'id'>; // 创建时无 id
type UpdateUserDTO = Partial<Omit<User, 'id'>>; // 更新时字段全可选类型守卫与类型收窄
类型收窄(Narrowing)是 TypeScript 日常使用中最重要的技能之一:当一个变量是联合类型时,如何在特定代码块里"缩小"到具体类型。
// 1. typeof 守卫(原始类型)
function format(value: string | number): string {
if (typeof value === 'number') {
return value.toFixed(2); // 这里 value 是 number
}
return value.toUpperCase(); // 这里 value 是 string
}
// 2. instanceof 守卫(类实例)
function logError(err: Error | string) {
if (err instanceof Error) {
console.log(err.stack); // err 是 Error
} else {
console.log(err); // err 是 string
}
}
// 3. in 守卫(判断属性是否存在)
type Fish = { swim: () => void };
type Bird = { fly: () => void };
function move(animal: Fish | Bird) {
if ('swim' in animal) {
animal.swim(); // Fish
} else {
animal.fly(); // Bird
}
}
// 4. 自定义类型守卫(is 关键字)
interface Cat { meow: () => void }
interface Dog { bark: () => void }
function isCat(pet: Cat | Dog): pet is Cat {
return 'meow' in pet;
}
function speak(pet: Cat | Dog) {
if (isCat(pet)) {
pet.meow();
} else {
pet.bark();
}
}
// 5. never 实现穷尽性检查
type Shape =
| { kind: 'circle'; radius: number }
| { kind: 'square'; side: number };
function area(shape: Shape): number {
switch (shape.kind) {
case 'circle': return Math.PI * shape.radius ** 2;
case 'square': return shape.side ** 2;
default:
// 若将来新增 shape 却忘了处理,这里会编译报错
const exhaustive: never = shape;
return exhaustive;
}
}高级类型
条件类型:
映射类型:
模板字面量类型:
代码示例
条件类型示例
// 基本条件类型
type IsString<T> = T extends string ? true : false;
type Test1 = IsString<string>; // true
type Test2 = IsString<number>; // false
// 条件类型与联合类型(分布式)
type NonNullableType<T> = T extends null | undefined ? never : T;
type Test3 = NonNullableType<string | null>; // string
// infer 关键字:提取函数返回值类型
type MyReturnType<T> = T extends (...args: any[]) => infer R ? R : never;
function foo(): string {
return 'hello';
}
type FooReturn = MyReturnType<typeof foo>; // string
// 提取数组元素类型
type ArrayElement<T> = T extends (infer U)[] ? U : never;
type NumberElement = ArrayElement<number[]>; // number
// 提取 Promise 的值类型
type PromiseValue<T> = T extends Promise<infer U> ? U : never;
type StringValue = PromiseValue<Promise<string>>; // string映射类型示例
// 基本映射类型
type MyReadonly<T> = {
readonly [P in keyof T]: T[P];
};
type MyPartial<T> = {
[P in keyof T]?: T[P];
};
interface User {
name: string;
age: number;
}
type ReadonlyUser = MyReadonly<User>;
type PartialUser = MyPartial<User>;
// 映射类型 + 键重映射:生成 getter
type Getters<T> = {
[P in keyof T as `get${Capitalize<string & P>}`]: () => T[P];
};
type PersonGetters = Getters<User>;
// {
// getName: () => string;
// getAge: () => number;
// }
// 映射类型 + 模板字面量类型:生成事件处理器
type EventHandlers<T> = {
[K in keyof T as `on${Capitalize<string & K>}`]?: (event: T[K]) => void;
};
interface Events {
click: MouseEvent;
change: Event;
}
type EventHandlersType = EventHandlers<Events>;
// {
// onClick?: (event: MouseEvent) => void;
// onChange?: (event: Event) => void;
// }模板字面量类型示例
// 基本模板字面量类型
type Greeting = `Hello, ${string}!`;
const greeting: Greeting = 'Hello, World!';
// 模板字面量类型 + 联合类型(笛卡尔积)
type Color = 'red' | 'green' | 'blue';
type Shade = 'light' | 'dark';
type ColorShade = `${Shade}-${Color}`;
const color1: ColorShade = 'light-red';
const color2: ColorShade = 'dark-blue';
// 事件名类型
type EventName<T extends string> = `on${Capitalize<T>}`;
type ClickEvent = EventName<'click'>; // 'onClick'
// 实际应用:CSS 自定义属性
type CSSVar<T extends string> = `--${T}`;
type PrimaryColorVar = CSSVar<'primary-color'>; // '--primary-color'模块系统
模块的概念:
导出方式: 命名导出、默认导出、重导出。
导入方式: 命名导入、默认导入、命名空间导入、动态导入。
代码示例
模块导出示例
// utils.ts
// 命名导出
export function add(a: number, b: number): number {
return a + b;
}
export function subtract(a: number, b: number): number {
return a - b;
}
// 默认导出
export default function multiply(a: number, b: number): number {
return a * b;
}
// 类型导出(推荐用 export type,编译时可被完全擦除)
export interface User {
id: number;
name: string;
}
export type ID = string | number;
// 重导出(聚合多个模块,常用于 index.ts)
export { add, subtract } from './math';
export * from './constants';
export type { Config } from './config';模块导入示例
// main.ts
// 命名导入
import { add, subtract } from './utils';
// 仅导入类型(type-only import,避免副作用、优化打包)
import type { User } from './utils';
// 默认导入
import multiply from './utils';
// 命名空间导入
import * as utils from './utils';
utils.add(1, 2);
// 混合导入
import multiply2, { add as sum } from './utils';
// 动态导入(按需加载,实现代码分割)
async function loadModule() {
const utils = await import('./utils');
console.log(utils.add(1, 2));
}tsconfig 与 strict 模式
`tsconfig.json` 控制编译行为。其中最重要的是 `strict` 系列开关,强烈建议新项目一律开启。
{
"compilerOptions": {
"target": "ES2020",
"module": "ESNext",
"strict": true, // 一键开启所有严格检查
"noImplicitAny": true, // 禁止隐式 any
"strictNullChecks": true, // null/undefined 必须显式处理
"noUnusedLocals": true, // 禁止未使用的变量
"noImplicitReturns": true, // 函数所有分支都要有返回
"esModuleInterop": true,
"skipLibCheck": true
}
}其中 `strictNullChecks` 收益最大:它把 `null` 和 `undefined` 从"任何类型的合法值"中剥离出来,逼你显式处理空值,能消灭绝大部分"Cannot read property of undefined"这类线上崩溃。
真实案例:用类型驱动重构 API 层
某团队的前端项目里,后端接口返回的数据结构散落在各处,字段拼写错误频发。引入 TypeScript 后,他们做了如下改造:
// 1. 用一份类型定义描述接口契约
interface UserDTO {
id: number;
userName: string; // 注意:后端是 userName 而非 username
createdAt: string;
}
// 2. 封装类型安全的请求函数
async function apiGet<T>(url: string): Promise<T> {
const res = await fetch(url);
if (!res.ok) throw new Error(`HTTP ${res.status}`);
return res.json() as Promise<T>;
}
// 3. 使用时自动获得字段提示与检查
const user = await apiGet<UserDTO>('/api/users/1');
console.log(user.userName); // IDE 自动提示,拼错立刻报红
// console.log(user.username); // 编译错误,拼写错误被拦截改造后,该模块因字段拼写、结构不一致导致的 bug 从每月十余个降到接近零,新人上手速度也明显提升——因为类型定义本身就是最新的接口文档。
常见坑
最佳实践
基础类型深入
前面的章节介绍了原始类型的写法,本节把最容易出坑的几个基础类型讲透:字面量类型、元组、枚举,以及 `any`/`unknown`/`never`/`void` 这四个特殊类型的区别。这些内容看似基础,却是 90% 类型 bug 的源头。
字面量类型与联合收窄
概念:字面量类型(Literal Type)把"具体的值"提升为类型。`'up'` 不再只是一个字符串值,而是一个只能取 `'up'` 这一个值的类型。它通常配合联合类型使用,用来建模"有限取值集合"。
原理:TypeScript 有两种字面量推断模式——宽化(Widening)和不宽化。用 `let` 声明时会被宽化成基础类型(`let x = 'up'` 推断为 `string`),用 `const` 声明时保持字面量类型(`const x = 'up'` 推断为 `'up'`)。这个差异是很多"为什么我的类型变宽了"问题的根源。
// let 宽化:类型变成 string
let a = 'up'; // 推断类型: string
// const 不宽化:类型是字面量 'up'
const b = 'up'; // 推断类型: 'up'
// 对象属性默认宽化
const config = { method: 'GET' }; // method 推断为 string
// 用 as const 冻结为字面量
const config2 = { method: 'GET' } as const; // method 为 'GET',且整个对象 readonly
// 字面量联合:建模有限状态
type HttpMethod = 'GET' | 'POST' | 'PUT' | 'DELETE' | 'PATCH';
function request(url: string, method: HttpMethod) {
// method 只能是这 5 个值之一
}
request('/api', 'GET'); // OK
// request('/api', 'get'); // Error: 'get' 不能赋给 HttpMethod
// 数字字面量联合
type DiceRoll = 1 | 2 | 3 | 4 | 5 | 6;
// 布尔字面量(较少单独用)
type Truthy = true;案例:假设一个按钮组件有 3 种尺寸、4 种变体。用字面量联合建模,调用方拼错字符串会在编译期直接报错,而不是等到运行时样式错乱。
type Size = 'small' | 'medium' | 'large';
type Variant = 'primary' | 'secondary' | 'danger' | 'ghost';
interface ButtonProps {
size: Size;
variant: Variant;
disabled?: boolean;
}
// 传错值立即报错,IDE 还会自动补全 3 个 size 选项
const props: ButtonProps = { size: 'medium', variant: 'primary' };常见坑:把可变对象传给需要字面量的参数时会报错,因为对象属性被宽化了。解决办法是加 `as const`,或显式标注类型。
const opts = { method: 'GET' };
// request('/api', opts.method); // Error: string 不能赋给 HttpMethod
const opts2 = { method: 'GET' } as const;
request('/api', opts2.method); // OK,method 为 'GET'元组(Tuple)
概念:元组是"长度固定、每个位置类型确定"的数组。普通数组 `number[]` 表示任意个 number,元组 `[number, string]` 表示恰好第 0 位是 number、第 1 位是 string。
原理:元组在运行时就是普通 JS 数组,类型信息只在编译期存在。TypeScript 4.0 起支持具名元组元素和可变元组(Variadic Tuple Types),大幅增强了表达能力。
// 基础元组
let pair: [string, number] = ['age', 25];
// pair = [25, 'age']; // Error: 位置类型不匹配
// 具名元组(4.0+):只是文档提示,不影响运行时
type Point = [x: number, y: number];
const p: Point = [10, 20];
// 可选元素
type Range = [start: number, end?: number];
const r1: Range = [0];
const r2: Range = [0, 100];
// 剩余元素
type StringThenNumbers = [string, ...number[]];
const s: StringThenNumbers = ['sum', 1, 2, 3, 4];
// 只读元组
type RO = readonly [number, number];
const ro: RO = [1, 2];
// ro[0] = 5; // Error: 只读
// 元组常见用法:模拟多返回值(类似 React useState)
function useToggle(init: boolean): [boolean, () => void] {
let state = init;
const toggle = () => { state = !state; };
return [state, toggle];
}
const [on, toggleOn] = useToggle(false);可变元组类型(Variadic Tuple) 让我们能对元组做泛型级别的拼接,是实现类型安全 `curry`、`concat` 的基础:
// 把两个元组拼起来
type Concat<T extends readonly unknown[], U extends readonly unknown[]> = [...T, ...U];
type R = Concat<[1, 2], [3, 4]>; // [1, 2, 3, 4]
// 提取首元素与剩余
type Head<T extends readonly unknown[]> = T extends [infer H, ...unknown[]] ? H : never;
type Tail<T extends readonly unknown[]> = T extends [unknown, ...infer Rest] ? Rest : [];
type H = Head<[1, 2, 3]>; // 1
type T2 = Tail<[1, 2, 3]>; // [2, 3]元组 vs 数组对比:
| 维度 | 数组 number[] | 元组 [number, string] |
| --- | --- | --- |
| 长度 | 任意 | 固定(除非有 rest) |
| 各位置类型 | 统一 | 可不同 |
| 越界访问 | 类型为元素类型 | 报错或 undefined |
| 典型场景 | 列表、集合 | 多返回值、坐标、键值对 |
| 解构提示 | 每项同类型 | 每位精确类型 |
常见坑:元组的 `push` 不会报错但会破坏长度约束——TypeScript 无法在类型层面阻止 `tuple.push()`。若要严格,用 `readonly` 元组。
枚举(Enum)深入
概念:枚举给一组命名常量赋予统一类型。TypeScript 有三种枚举:数字枚举、字符串枚举、`const enum`。它们的编译产物差异巨大,是面试高频考点。
原理与编译产物:普通枚举会编译成一个真实的对象(IIFE),既能正向映射也能反向映射;`const enum` 在编译期被内联,不生成任何运行时对象。
// 数字枚举:默认从 0 递增
enum Direction {
Up, // 0
Down, // 1
Left, // 2
Right, // 3
}
// 可手动赋起始值,后续递增
enum StatusCode {
OK = 200,
NotFound = 404,
ServerError = 500,
}
// 字符串枚举:无自增,每个都要赋值
enum LogLevel {
Debug = 'DEBUG',
Info = 'INFO',
Warn = 'WARN',
Error = 'ERROR',
}数字枚举的编译产物(关键:反向映射):
// 上面的 Direction 编译成大致如下 JS:
var Direction;
(function (Direction) {
Direction[Direction["Up"] = 0] = "Up";
Direction[Direction["Down"] = 1] = "Down";
Direction[Direction["Left"] = 2] = "Left";
Direction[Direction["Right"] = 3] = "Right";
})(Direction || (Direction = {}));
// 结果:Direction.Up === 0,且 Direction[0] === 'Up'(反向映射)
// 字符串枚举只有正向映射,没有反向映射`const enum` 的内联:
const enum Color { Red, Green, Blue }
const c = Color.Green;
// 编译后直接内联为:const c = 1;(不生成 Color 对象)
// 优点:零运行时开销;缺点:无法反向映射、跨模块/isolatedModules 下有坑枚举 vs 联合字面量对比(现代实践更推荐后者):
| 维度 | enum | as const 对象 / 字面量联合 |
| --- | --- | --- |
| 运行时产物 | 有对象(const enum 除外) | as const 有对象,纯联合无 |
| 反向映射 | 数字枚举支持 | 不支持 |
| Tree-shaking | 差(普通 enum) | 好 |
| 类型收窄 | 支持 | 支持且更直观 |
| 与 JS 生态兼容 | 一般 | 好 |
| 推荐度 | 逐渐降低 | 逐渐升高 |
替代枚举的现代写法:
// 用 as const 对象替代枚举,既有运行时值又有类型
const Direction = {
Up: 'UP',
Down: 'DOWN',
Left: 'LEFT',
Right: 'RIGHT',
} as const;
type Direction = typeof Direction[keyof typeof Direction]; // 'UP' | 'DOWN' | 'LEFT' | 'RIGHT'
function move(dir: Direction) { /* ... */ }
move(Direction.Up); // OK
move('UP'); // 也 OK,因为就是字符串字面量常见坑:
最佳实践:新项目优先用字符串枚举或 `as const` 对象;库作者若产物要被 Babel 处理,避免 `const enum`。
any / unknown / never / void 四大特殊类型
这四个类型最容易混淆,一张表讲清区别:
| 类型 | 含义 | 可赋值给它 | 它可赋值给 | 典型场景 |
| --- | --- | --- | --- | --- |
| any | 关闭类型检查 | 任何值 | 任何类型 | 迁移期、逃生舱(应尽量避免) |
| unknown | 类型安全的顶层类型 | 任何值 | 只能给 unknown/any | 接收未知输入,用前必须收窄 |
| never | 不可能有值的底层类型 | 无(除 never) | 任何类型 | 穷尽检查、抛异常函数返回 |
| void | 没有有意义的返回值 | undefined(及 null 非严格) | 只能给 void/any/unknown | 函数无返回、回调忽略返回值 |
any vs unknown:`any` 会传染并关闭检查,`unknown` 强制你先收窄再使用。
function parseAny(json: string): any {
return JSON.parse(json);
}
const a = parseAny('{}');
a.foo.bar.baz; // 不报错,运行时可能崩——any 的危险
function parseUnknown(json: string): unknown {
return JSON.parse(json);
}
const u = parseUnknown('{}');
// u.foo; // Error: 对象类型为 unknown
if (typeof u === 'object' && u !== null && 'foo' in u) {
// 收窄后才能访问
}never 的用途:表示"永远不会发生"。抛异常或死循环的函数返回 `never`;穷尽检查时用它保证覆盖所有分支。
// 返回 never 的函数
function fail(msg: string): never {
throw new Error(msg);
}
function loop(): never {
while (true) {}
}
// never 用于穷尽检查(exhaustive check)
type Shape =
| { kind: 'circle'; r: number }
| { kind: 'square'; size: number };
function area(s: Shape): number {
switch (s.kind) {
case 'circle': return Math.PI * s.r ** 2;
case 'square': return s.size ** 2;
default:
// 若将来给 Shape 加了新成员却忘了处理,这里会编译报错
const _exhaustive: never = s;
return _exhaustive;
}
}void 的细节:`void` 类型的函数并非"不能返回值",而是"返回值不该被使用"。这让 `Array.forEach` 能接受返回任意值的回调。
type VoidFn = () => void;
const fn: VoidFn = () => 42; // OK!返回值被忽略
// 但你不能利用它的返回值
const r = fn(); // r 的类型是 void
// 实用场景:forEach 回调可以直接写 arr.push(x) 而不报错
[1, 2, 3].forEach(x => console.log(x));常见坑:把 `() => void` 的类型标注理解成"必须无返回",导致误判。实际它只是"调用方不看返回值"。
最佳实践:把 `any` 当最后手段,优先 `unknown`;用 `never` 做穷尽检查兜底;理解 `void` 的"忽略返回值"语义。
小结表
| 类型 | 一句话记忆 | 关键坑 |
| --- | --- | --- |
| 字面量类型 | 值即类型,配 as const 冻结 | let 宽化 |
| 元组 | 定长定序的数组 | push 破坏长度 |
| 数字枚举 | 有反向映射、运行时对象 | 可赋任意 number |
| 字符串枚举 | 只有正向映射 | 无自增 |
| const enum | 编译期内联零开销 | isolatedModules 冲突 |
| any | 关闭检查,会传染 | 尽量别用 |
| unknown | 安全版 any,用前收窄 | 不能直接访问 |
| never | 不可能的值 | 穷尽检查兜底 |
| void | 忽略返回值 | 不是"禁止返回" |
interface 与 type 深度对比
`interface` 和 `type` 是 TypeScript 里定义对象结构的两把利器,功能高度重叠却又各有专长。理解它们的差异,能让你在团队规范和库设计中做出正确取舍。
核心差异总览
| 维度 | interface | type |
| --- | --- | --- |
| 描述对象/类结构 | 擅长 | 可以 |
| 联合类型 | 不能 | 能(A \| B) |
| 交叉类型 | 用 extends 组合 | 用 & 组合 |
| 声明合并 | 支持(同名自动合并) | 不支持(重名报错) |
| 计算属性/映射类型 | 不支持 | 支持 |
| 元组、原始类型别名 | 不支持 | 支持 |
| 被 class implements | 支持 | 支持(非联合时) |
| 扩展性(库对外 API) | 更适合(可被外部合并扩展) | 相对封闭 |
| 错误信息可读性 | 通常更友好 | 复杂 type 可能很长 |
| 性能(大型项目编译) | 略优(可缓存) | 复杂交叉略慢 |
interface 能做而 type 做不到的:声明合并
概念:声明合并(Declaration Merging)指同名 `interface` 会自动合并成一个。这在为第三方库或全局对象扩展类型时极其有用。
// 同名 interface 自动合并
interface User {
name: string;
}
interface User {
age: number;
}
// 合并后 User = { name: string; age: number }
const u: User = { name: 'Alice', age: 25 };
// 经典用法:扩展全局 Window
declare global {
interface Window {
myAppConfig: { version: string };
}
}
window.myAppConfig = { version: '1.0.0' };
// 扩展第三方库的类型(如给 Express Request 加字段)
// declare module 'express' {
// interface Request { userId?: string; }
// }`type` 无法这样合并——同名 `type` 会直接报"重复标识符"。
type 能做而 interface 做不到的
// 1. 联合类型
type Result = { ok: true; data: string } | { ok: false; error: string };
// 2. 原始类型/元组别名
type ID = string | number;
type Pair = [number, number];
// 3. 映射类型
type Optional<T> = { [K in keyof T]?: T[K] };
// 4. 模板字面量类型
type EventName = `on${string}`;
// 5. 条件类型
type NonNull<T> = T extends null | undefined ? never : T;扩展方式对比
// interface 用 extends(可多继承)
interface Animal { name: string; }
interface Pet extends Animal { owner: string; }
interface Dog extends Animal, Pet { breed: string; }
// type 用交叉 &
type Animal2 = { name: string };
type Pet2 = Animal2 & { owner: string };
// 互相扩展也可以
interface FromType extends Animal2 { extra: boolean; }
type FromInterface = Animal & { extra: boolean };extends 与 & 的细微差别:`interface extends` 遇到同名不兼容属性会立即报错;`type &` 遇到冲突属性会得到 `never` 而不报错,可能埋坑。
interface A { x: number }
// interface B extends A { x: string } // Error: 类型不兼容,立即暴露
type C = { x: number } & { x: string }; // x 变成 never,不报错但已损坏索引签名、只读、可选、函数与构造签名
索引签名(Index Signature) 描述"键类型统一、数量不定"的对象:
interface StringMap {
[key: string]: string;
}
const headers: StringMap = { 'Content-Type': 'application/json' };
// 数字索引签名(数组式)
interface NumberList {
[index: number]: string;
}
// 混合:具体属性 + 索引签名(具体属性类型必须兼容索引类型)
interface Dict {
length: number; // OK
[key: string]: number; // 所有键都必须是 number
}只读与可选修饰符:
interface Config {
readonly id: string; // 只读,初始化后不可改
name: string; // 必填
description?: string; // 可选
readonly tags?: string[]; // 只读且可选
}
const cfg: Config = { id: '1', name: 'app' };
// cfg.id = '2'; // Error: 只读属性
cfg.name = 'newName'; // OK函数签名与构造签名:接口不仅能描述对象,还能描述可调用/可 new 的类型。
// 函数类型接口
interface Adder {
(a: number, b: number): number; // 调用签名
version: string; // 函数也能带属性
}
const add: Adder = Object.assign(
(a: number, b: number) => a + b,
{ version: '1.0' }
);
// 构造签名(描述可被 new 的类型)
interface PointCtor {
new (x: number, y: number): { x: number; y: number };
}
function createPoint(Ctor: PointCtor, x: number, y: number) {
return new Ctor(x, y);
}
// 重载签名
interface StringOrNumber {
(value: string): string;
(value: number): number;
}常见坑:索引签名会"淹没"具体属性的类型安全——一旦有 `[key: string]: number`,访问任意字符串键都被认为返回 number,即使那个键不存在。
最佳实践:
小结表
| 需求 | 用 interface | 用 type |
| --- | --- | --- |
| 对象结构 | 首选 | 可以 |
| 联合类型 | 不行 | 必须 |
| 声明合并扩展库 | 必须 | 不行 |
| 映射/条件/模板类型 | 不行 | 必须 |
| class implements | 都行 | 都行 |
类(Class)深入
TypeScript 的类在 ES2015 class 基础上叠加了访问修饰符、参数属性、抽象类、`implements`、存取器等类型化能力。
访问修饰符
概念:`public`(默认,处处可访问)、`private`(仅类内部)、`protected`(类及子类内部)、`readonly`(初始化后只读)。
class BankAccount {
public owner: string; // 默认 public,可省略
private balance: number; // 仅类内部可访问
protected pin: string; // 类及子类可访问
readonly accountId: string; // 只读
constructor(owner: string, accountId: string, pin: string) {
this.owner = owner;
this.accountId = accountId;
this.pin = pin;
this.balance = 0;
}
deposit(amount: number) {
this.balance += amount; // OK:类内部
}
getBalance() { return this.balance; }
}
const acc = new BankAccount('Alice', 'A001', '1234');
acc.deposit(100);
// acc.balance; // Error: private
// acc.accountId = 'x'; // Error: readonly关键区别:TypeScript 的 `private` 是"编译期私有",编译后属性依然存在于对象上,运行时可被访问(如 `(acc as any).balance`)。而 ES 私有字段 `#` 是"运行时真私有"。
参数属性(Parameter Properties)
原理:在构造函数参数上加修饰符,TypeScript 会自动声明并赋值同名属性,省去样板代码。
// 传统写法
class Point1 {
x: number;
y: number;
constructor(x: number, y: number) {
this.x = x;
this.y = y;
}
}
// 参数属性写法(等价,代码量减少约 60%)
class Point2 {
constructor(
public x: number,
public y: number,
private readonly label: string = 'point'
) {}
describe() { return `${this.label}(${this.x}, ${this.y})`; }
}
const pt = new Point2(1, 2);抽象类与 implements
概念:`abstract` 类不能被实例化,只能被继承;抽象方法只有签名没有实现,强制子类实现。`implements` 让类承诺满足某个接口。
abstract class Shape {
abstract area(): number; // 抽象方法,子类必须实现
abstract name: string; // 抽象属性
describe(): string { // 具体方法,可被继承
return `${this.name} 面积为 ${this.area()}`;
}
}
class Circle extends Shape {
name = 'circle';
constructor(private r: number) { super(); }
area(): number { return Math.PI * this.r ** 2; }
}
// new Shape(); // Error: 抽象类不能实例化
const c = new Circle(5);
console.log(c.describe());
// implements:类必须满足接口结构
interface Serializable {
serialize(): string;
}
class User implements Serializable {
constructor(public name: string) {}
serialize() { return JSON.stringify({ name: this.name }); }
}abstract class vs interface 对比:
| 维度 | abstract class | interface |
| --- | --- | --- |
| 可含实现 | 能(具体方法/字段) | 不能(仅签名) |
| 运行时产物 | 有(是真实的类) | 无(编译擦除) |
| 多继承 | 单继承 | 类可 implements 多个 |
| 构造函数 | 有 | 无 |
| 适用 | 共享实现的基类 | 纯契约约束 |
存取器、静态成员与私有字段 #
class Temperature {
private _celsius = 0;
// getter/setter:像属性一样访问,内部可加校验
get celsius(): number { return this._celsius; }
set celsius(value: number) {
if (value < -273.15) throw new Error('低于绝对零度');
this._celsius = value;
}
get fahrenheit(): number { return this._celsius * 9 / 5 + 32; }
// 静态成员:属于类本身,不属于实例
static readonly ABSOLUTE_ZERO = -273.15;
static fromFahrenheit(f: number): Temperature {
const t = new Temperature();
t.celsius = (f - 32) * 5 / 9;
return t;
}
}
const t = new Temperature();
t.celsius = 25; // 调用 setter
console.log(t.fahrenheit); // 调用 getter
console.log(Temperature.ABSOLUTE_ZERO);
// ES 私有字段 #:运行时真私有
class Counter {
#count = 0; // 外部完全无法访问,连 as any 都不行
increment() { this.#count++; }
get value() { return this.#count; }
}
const counter = new Counter();
counter.increment();
// counter.#count; // 语法错误:私有字段只能在类内部访问private vs # 对比:
| 维度 | TS private | ES #私有字段 |
| --- | --- | --- |
| 私有级别 | 编译期 | 运行时真私有 |
| 运行时可绕过 | 能(as any) | 不能 |
| 编译产物 | 普通属性 | WeakMap 或原生 #(视 target) |
| 子类访问 | protected 可 | 不可 |
| 命名冲突 | 按名字 | 独立命名空间 |
常见坑:
最佳实践:需要运行时封装用 `#`;共享实现的层次结构用抽象类;纯契约用接口 + `implements`;用参数属性精简构造函数。
小结表
| 特性 | 用途 | 记忆点 |
| --- | --- | --- |
| public/private/protected | 访问控制 | private 仅编译期 |
| readonly | 初始化后只读 | 不影响引用内部可变 |
| 参数属性 | 精简构造赋值 | 修饰符写在参数上 |
| abstract | 强制子类实现 | 不可实例化 |
| implements | 类满足接口契约 | 只检查不继承实现 |
| #私有字段 | 运行时真私有 | 独立命名空间 |
| static | 类级成员 | 不访问实例 this |
泛型深入
泛型(Generics)是"类型的参数化"——把类型当参数传递,让函数、类、类型在保持类型安全的同时复用于多种类型。泛型是 TypeScript 类型系统的引擎。
泛型约束(Constraints)
概念:默认的泛型 `T` 可以是任何类型,你不能对它做任何假设。用 `extends` 给泛型加约束,就能安全地访问其成员。
// 无约束:不能访问 .length,因为 T 可能是 number
// function longest<T>(a: T, b: T) { return a.length > b.length ? a : b; } // Error
// 加约束:T 必须有 length 属性
function longest<T extends { length: number }>(a: T, b: T): T {
return a.length >= b.length ? a : b;
}
longest([1, 2], [1, 2, 3]); // OK,返回 number[]
longest('ab', 'abc'); // OK,返回 string
// longest(1, 2); // Error: number 没有 length
// keyof 约束:K 必须是 T 的键
function getProp<T, K extends keyof T>(obj: T, key: K): T[K] {
return obj[key];
}
const person = { name: 'Alice', age: 25 };
const name = getProp(person, 'name'); // 类型精确为 string
const age = getProp(person, 'age'); // 类型精确为 number
// getProp(person, 'email'); // Error: 'email' 不是键泛型默认参数
// 泛型默认值:不传时用默认类型
interface ApiResponse<T = unknown> {
code: number;
data: T;
message: string;
}
const r1: ApiResponse = { code: 0, data: null, message: 'ok' }; // T = unknown
const r2: ApiResponse<string[]> = { code: 0, data: [], message: '' };// T = string[]
// 多个泛型参数,后者可引用前者
class Container<T, U = T[]> {
constructor(public item: T, public list: U) {}
}泛型工具函数与泛型类
// 泛型函数:类型随参数流动
function identity<T>(x: T): T { return x; }
const n = identity(42); // n: number(推断)
const s = identity<string>('hi'); // 显式指定
// 泛型数组工具
function first<T>(arr: T[]): T | undefined { return arr[0]; }
function map<T, U>(arr: T[], fn: (item: T, index: number) => U): U[] {
const result: U[] = [];
for (let i = 0; i < arr.length; i++) result.push(fn(arr[i], i));
return result;
}
const lengths = map(['a', 'bb', 'ccc'], s => s.length); // number[]
// 泛型类:类型安全的栈
class Stack<T> {
private items: T[] = [];
push(item: T): void { this.items.push(item); }
pop(): T | undefined { return this.items.pop(); }
peek(): T | undefined { return this.items[this.items.length - 1]; }
get size(): number { return this.items.length; }
}
const numStack = new Stack<number>();
numStack.push(1);
numStack.push(2);
const top = numStack.pop(); // number | undefined条件类型与 infer
概念:条件类型 `T extends U ? X : Y` 是"类型层面的三元表达式"。`infer` 关键字能在条件类型里"捕获"某个位置的类型并命名。
// 基础条件类型
type IsString<T> = T extends string ? 'yes' : 'no';
type A = IsString<'hello'>; // 'yes'
type B = IsString<42>; // 'no'
// infer 捕获:提取数组元素类型
type ElementType<T> = T extends (infer E)[] ? E : never;
type E1 = ElementType<number[]>; // number
type E2 = ElementType<string[]>; // string
// infer 捕获函数返回值(ReturnType 的原理)
type MyReturnType<T> = T extends (...args: any[]) => infer R ? R : never;
type R1 = MyReturnType<() => number>; // number
type R2 = MyReturnType<(x: string) => void>; // void
// infer 捕获 Promise 解包
type Unwrap<T> = T extends Promise<infer U> ? U : T;
type U1 = Unwrap<Promise<string>>; // string
type U2 = Unwrap<number>; // number
// 嵌套 infer:提取第一个函数参数类型
type FirstArg<T> = T extends (first: infer F, ...rest: any[]) => any ? F : never;
type F1 = FirstArg<(name: string, age: number) => void>; // string分布式条件类型(Distributive Conditional Types)
原理:当条件类型作用于"裸的泛型参数"且该参数是联合类型时,会自动"分发"到联合的每个成员上,分别计算再合并。这是很多工具类型的底层机制。
// 分布式:联合被逐个处理
type ToArray<T> = T extends any ? T[] : never;
type R = ToArray<string | number>; // string[] | number[](分发了!)
// 计算过程:ToArray<string> | ToArray<number> = string[] | number[]
// 用 [T] 包裹可阻止分发
type ToArrayNonDist<T> = [T] extends [any] ? T[] : never;
type R2 = ToArrayNonDist<string | number>; // (string | number)[]
// 利用分发实现 Exclude
type MyExclude<T, U> = T extends U ? never : T;
type Without = MyExclude<'a' | 'b' | 'c', 'a'>; // 'b' | 'c'
// 过程:('a' extends 'a' ? never : 'a') | ('b' ...) | ('c' ...)
// = never | 'b' | 'c' = 'b' | 'c'分发与否对比:
| 写法 | 是否分发 | ToArray 结果 |
| --- | --- | --- |
| T extends any ? T[] : never | 分发 | A[] \| B[] |
| [T] extends [any] ? T[] : never | 不分发 | (A\|B)[] |
常见坑:
最佳实践:需要逐成员处理联合时依赖分发;需要整体判断时用 `[T] extends [U]` 关闭分发。
小结表
| 特性 | 作用 | 关键词 |
| --- | --- | --- |
| extends 约束 | 限定泛型可取范围 | keyof、结构约束 |
| 默认参数 | 泛型省略时兜底 | T = unknown |
| 条件类型 | 类型层三元 | T extends U ? X : Y |
| infer | 捕获并命名类型 | 返回值、元素、Promise |
| 分布式条件 | 联合逐成员处理 | 裸类型才分发 |
类型收窄与类型守卫
联合类型让变量"可能是多种类型之一",而类型守卫(Type Guard)让 TypeScript 在特定代码块内把它收窄(Narrowing)到更具体的类型。这是写出健壮 TS 代码的核心技能。
typeof、instanceof、in、字面量收窄
// typeof 收窄(用于原始类型)
function format(value: string | number): string {
if (typeof value === 'string') {
return value.toUpperCase(); // 此处 value 收窄为 string
}
return value.toFixed(2); // 此处 value 收窄为 number
}
// instanceof 收窄(用于类实例)
class Dog { bark() {} }
class Cat { meow() {} }
function speak(animal: Dog | Cat) {
if (animal instanceof Dog) {
animal.bark(); // 收窄为 Dog
} else {
animal.meow(); // 收窄为 Cat
}
}
// in 收窄(按属性存在性)
type Fish = { swim: () => void };
type Bird = { fly: () => void };
function move(animal: Fish | Bird) {
if ('swim' in animal) {
animal.swim(); // 收窄为 Fish
} else {
animal.fly(); // 收窄为 Bird
}
}
// 字面量与真值收窄
function process(input: string | null) {
if (input) {
input.trim(); // 收窄为 string(排除了 null 和 '')
}
}自定义类型谓词 is
概念:返回类型写成 `arg is Type` 的函数是"类型谓词函数",调用后 TypeScript 会据其真假收窄类型。适合封装复杂的判断逻辑。
interface Admin { role: 'admin'; permissions: string[]; }
interface Guest { role: 'guest'; }
type Account = Admin | Guest;
// 类型谓词:返回 account is Admin
function isAdmin(account: Account): account is Admin {
return account.role === 'admin';
}
function handle(account: Account) {
if (isAdmin(account)) {
console.log(account.permissions); // 收窄为 Admin,可访问 permissions
}
}
// 用于过滤数组并收窄元素类型
function isDefined<T>(value: T | undefined | null): value is T {
return value !== undefined && value !== null;
}
const mixed = [1, undefined, 2, null, 3];
const cleaned = mixed.filter(isDefined); // number[](去掉了 undefined/null)断言函数 asserts
概念:`asserts` 让函数在"不满足条件时抛错",之后的代码里 TypeScript 认为条件成立。分两种:`asserts cond` 和 `asserts x is Type`。
// asserts 条件:断言后续代码里条件为真
function assert(condition: unknown, msg?: string): asserts condition {
if (!condition) throw new Error(msg ?? '断言失败');
}
function getLength(value: string | null): number {
assert(value !== null, 'value 不能为空');
return value.length; // 此后 value 收窄为 string
}
// asserts x is Type:断言 x 是某类型
function assertIsString(val: unknown): asserts val is string {
if (typeof val !== 'string') throw new Error('不是字符串');
}
function useValue(input: unknown) {
assertIsString(input);
input.toUpperCase(); // input 收窄为 string
}可辨识联合与 never 穷尽检查
概念:可辨识联合(Discriminated Union)是给联合的每个成员加一个共同的"判别字段"(如 `kind`/`type`),配合 `switch` 收窄,是建模状态机的黄金模式。
// 建模异步请求的四种状态
type RequestState<T> =
| { status: 'idle' }
| { status: 'loading' }
| { status: 'success'; data: T }
| { status: 'error'; error: string };
function render<T>(state: RequestState<T>): string {
switch (state.status) {
case 'idle': return '空闲';
case 'loading': return '加载中...';
case 'success': return `数据:${JSON.stringify(state.data)}`; // 可访问 data
case 'error': return `错误:${state.error}`; // 可访问 error
default:
// 穷尽检查:若漏了某个 status,这里编译报错
const _exhaustive: never = state;
return _exhaustive;
}
}为什么 never 穷尽检查重要:将来给 `RequestState` 加一个 `{ status: 'cancelled' }`,若忘了在 switch 里处理,`state` 在 default 分支就不再是 `never`,赋值给 `_exhaustive: never` 会立即编译报错,强制你补全逻辑。这是"用类型系统防止漏改"的经典手段。
收窄手段对比表
| 守卫方式 | 适用类型 | 示例 | 局限 |
| --- | --- | --- | --- |
| typeof | 原始类型 | typeof x === 'string' | 只有 8 种返回值 |
| instanceof | 类实例 | x instanceof Date | 需有构造函数 |
| in | 对象属性 | 'swim' in x | 靠属性名 |
| 真值/字面量 | 联合含 null 等 | if (x) | 注意 0、''、false |
| is 谓词 | 任意 | x is Admin | 逻辑自己保证正确 |
| asserts | 任意 | asserts x is T | 不满足则抛错 |
| 可辨识联合 | 带判别字段的联合 | switch(x.kind) | 需设计判别字段 |
常见坑:
最佳实践:优先用可辨识联合建模状态;复杂判断封装成 `is` 谓词;用 `never` 兜底保证穷尽;避免用真值收窄处理可能为 0/'' 的值。
小结表
| 技术 | 一句话 | 记忆 |
| --- | --- | --- |
| typeof/instanceof/in | 内置收窄 | 分别管原始/实例/属性 |
| is 谓词 | 自定义收窄 | 返回 x is T |
| asserts | 断言收窄 | 不满足抛错 |
| 可辨识联合 | 状态建模 | 判别字段 + switch |
| never 穷尽 | 防漏改 | default 赋值 never |
内置工具类型全解与手写实现
TypeScript 内置了一批工具类型(Utility Types),能从已有类型派生出新类型。理解它们的源码实现,是掌握类型编程的最好途径。下面逐个给出用法、手写实现和逐行解释。
Partial / Required / Readonly
// Partial<T>:把所有属性变可选
type MyPartial<T> = {
[P in keyof T]?: T[P]; // 遍历 T 的每个键 P,加 ? 变可选
};
// Required<T>:把所有属性变必填(-? 去掉可选修饰符)
type MyRequired<T> = {
[P in keyof T]-?: T[P]; // -? 移除可选标记
};
// Readonly<T>:把所有属性变只读
type MyReadonly<T> = {
readonly [P in keyof T]: T[P]; // 每个键加 readonly
};
interface User { id: number; name: string; email?: string; }
type PartialUser = MyPartial<User>; // { id?; name?; email? }
type RequiredUser = MyRequired<User>; // { id; name; email }(email 变必填)
type ReadonlyUser = MyReadonly<User>; // 全部只读逐行解释:`[P in keyof T]` 是映射类型语法,`keyof T` 得到所有键的联合(如 `'id' | 'name' | 'email'`),`P in` 逐个遍历。`?`/`-?`/`readonly` 是修饰符,`+`/`-` 分别表示添加/移除。`T[P]` 是索引访问,取出该键对应的值类型。
Pick / Omit
// Pick<T, K>:从 T 中挑选 K 指定的键
type MyPick<T, K extends keyof T> = {
[P in K]: T[P]; // 只遍历 K(K 被约束为 T 的键子集)
};
// Omit<T, K>:从 T 中排除 K 指定的键
type MyOmit<T, K extends keyof any> = MyPick<T, Exclude<keyof T, K>>;
// 原理:先用 Exclude 从 keyof T 里去掉 K,再 Pick 剩下的
type UserPreview = MyPick<User, 'id' | 'name'>; // { id; name }
type UserNoEmail = MyOmit<User, 'email'>; // { id; name }Omit 的两步拆解:`Exclude
Record
// Record<K, V>:构造键为 K、值为 V 的对象类型
type MyRecord<K extends keyof any, V> = {
[P in K]: V; // 遍历 K 的每个成员,值都设为 V
};
type PageInfo = { title: string };
type Pages = MyRecord<'home' | 'about' | 'contact', PageInfo>;
// { home: PageInfo; about: PageInfo; contact: PageInfo }
// 常用于字典/映射
type Scores = Record<string, number>;
const s: Scores = { math: 90, english: 85 };Exclude / Extract / NonNullable
// Exclude<T, U>:从联合 T 中去掉可赋给 U 的成员
type MyExclude<T, U> = T extends U ? never : T;
// Extract<T, U>:从联合 T 中保留可赋给 U 的成员
type MyExtract<T, U> = T extends U ? T : never;
// NonNullable<T>:去掉 null 和 undefined
type MyNonNullable<T> = T extends null | undefined ? never : T;
type T1 = MyExclude<'a' | 'b' | 'c', 'a'>; // 'b' | 'c'
type T2 = MyExtract<'a' | 'b' | 'c', 'a' | 'z'>; // 'a'
type T3 = MyNonNullable<string | null | undefined>; // string原理:这三者都利用了"分布式条件类型"——联合被逐成员判断,`never` 会从结果联合中消失,从而实现过滤效果。
ReturnType / Parameters / ConstructorParameters / InstanceType
// ReturnType<T>:提取函数返回值类型
type MyReturnType<T extends (...args: any) => any> =
T extends (...args: any) => infer R ? R : never;
// Parameters<T>:提取函数参数类型(作为元组)
type MyParameters<T extends (...args: any) => any> =
T extends (...args: infer P) => any ? P : never;
// InstanceType<T>:提取构造函数返回的实例类型
type MyInstanceType<T extends abstract new (...args: any) => any> =
T extends abstract new (...args: any) => infer R ? R : never;
function createUser(id: number, name: string) {
return { id, name, createdAt: Date.now() };
}
type CreateUserReturn = MyReturnType<typeof createUser>; // { id; name; createdAt }
type CreateUserArgs = MyParameters<typeof createUser>; // [number, string]
class Widget { constructor(public w: number) {} }
type W = MyInstanceType<typeof Widget>; // WidgetAwaited(解包 Promise,4.5+)
// 简化版 Awaited:递归解包嵌套 Promise
type MyAwaited<T> =
T extends Promise<infer U>
? MyAwaited<U> // 若解包后还是 Promise,继续递归
: T;
type A1 = MyAwaited<Promise<string>>; // string
type A2 = MyAwaited<Promise<Promise<number>>>; // number(递归解到底)
type A3 = MyAwaited<boolean>; // boolean
// 实战:拿到 async 函数的真实返回值
async function fetchUser() { return { id: 1, name: 'Alice' }; }
type FetchedUser = Awaited<ReturnType<typeof fetchUser>>; // { id; name }内置工具类型速查表
| 工具类型 | 作用 | 实现核心 |
| --- | --- | --- |
| Partial
| Required
| Readonly
| Pick
| Omit
| Record
| Exclude
| Extract
| NonNullable
| ReturnType
| Parameters
| InstanceType
| Awaited
常见坑:
高级类型与类型体操
映射类型、键重映射、模板字面量类型、递归类型组合起来,能在类型层面做相当复杂的"编程",俗称"类型体操"。
映射类型与键重映射(as)
概念:映射类型遍历一个类型的键生成新类型;TypeScript 4.1 引入的键重映射(Key Remapping)允许用 `as` 子句改写键名,甚至过滤键。
// 基础映射 + 修饰符增删
type Mutable<T> = {
-readonly [P in keyof T]: T[P]; // 移除 readonly
};
// 键重映射:给每个键加 get 前缀,生成 getter 类型
type Getters<T> = {
[P in keyof T as `get${Capitalize<string & P>}`]: () => T[P];
};
interface Person { name: string; age: number; }
type PersonGetters = Getters<Person>;
// { getName: () => string; getAge: () => number }
// 键重映射过滤:用 never 剔除不想要的键
type RemoveKind<T> = {
[P in keyof T as P extends 'kind' ? never : P]: T[P];
};
type WithoutKind = RemoveKind<{ kind: string; value: number }>; // { value: number }
// 只保留值为函数的键(提取方法)
type MethodsOnly<T> = {
[P in keyof T as T[P] extends Function ? P : never]: T[P];
};模板字面量类型
概念:模板字面量类型把字符串操作带到了类型层面,可以拼接、约束字符串格式,配合内置的 `Uppercase`/`Lowercase`/`Capitalize`/`Uncapitalize`。
// 基础拼接
type Greeting = `Hello, ${string}!`;
const g1: Greeting = 'Hello, world!'; // OK
// const g2: Greeting = 'Hi'; // Error
// 联合分发式组合(笛卡尔积)
type Color = 'red' | 'blue';
type Shade = 'light' | 'dark';
type ColorShade = `${Shade}-${Color}`;
// 'light-red' | 'light-blue' | 'dark-red' | 'dark-blue'
// 内置字符串操作
type EventName<T extends string> = `on${Capitalize<T>}`;
type ClickEvent = EventName<'click'>; // 'onClick'
type Loud = Uppercase<'hello'>; // 'HELLO'
type Quiet = Lowercase<'HELLO'>; // 'hello'
// 结合 infer 解析字符串
type ExtractRouteParam<T extends string> =
T extends `/${string}/:${infer Param}` ? Param : never;
type Param = ExtractRouteParam<'/users/:id'>; // 'id'递归类型与类型体操实例
概念:类型可以引用自身,形成递归。配合条件类型和 `infer`,能实现深层变换。
// DeepReadonly:递归只读
type DeepReadonly<T> = {
readonly [P in keyof T]: T[P] extends object
? DeepReadonly<T[P]> // 值是对象则递归
: T[P];
};
interface Nested { a: { b: { c: number } }; }
type FrozenNested = DeepReadonly<Nested>; // 所有层级只读
// DeepPartial:递归可选
type DeepPartial<T> = {
[P in keyof T]?: T[P] extends object ? DeepPartial<T[P]> : T[P];
};
// 类型体操:数组长度(利用元组 length)
type Length<T extends readonly any[]> = T['length'];
type L = Length<[1, 2, 3]>; // 3
// 类型体操:字符串转联合
type StringToUnion<S extends string> =
S extends `${infer First}${infer Rest}`
? First | StringToUnion<Rest>
: never;
type Chars = StringToUnion<'abc'>; // 'a' | 'b' | 'c'
// 类型体操:反转元组
type Reverse<T extends any[]> =
T extends [infer First, ...infer Rest]
? [...Reverse<Rest>, First]
: [];
type Reversed = Reverse<[1, 2, 3]>; // [3, 2, 1]递归深度限制:TypeScript 对类型递归有深度上限(默认约 50 层实例化深度,尾递归优化后可到约 1000 层)。写类型体操时超限会报 "Type instantiation is excessively deep and possibly infinite"。
高级类型能力对比
| 能力 | 语法 | 典型用途 |
| --- | --- | --- |
| 映射类型 | [P in keyof T] | 批量变换属性 |
| 键重映射 | as 新键名 | 改键名、过滤键 |
| 模板字面量类型 | `${A}-${B}` | 字符串格式约束 |
| 递归类型 | 类型引用自身 | DeepPartial、解析 |
| 条件+infer | extends ? : + infer | 提取、分解类型 |
常见坑:
最佳实践:类型体操适度使用,优先可读性;复杂类型加注释说明意图;能用内置工具类型就别重复造轮子。
小结表
| 技术 | 一句话 | 记忆 |
| --- | --- | --- |
| 映射类型 | 遍历键生成新类型 | [P in keyof T] |
| 键重映射 as | 改写/过滤键 | never 剔除 |
| 模板字面量类型 | 类型级字符串 | 笛卡尔积 |
| 递归类型 | 深层变换 | 注意深度限制 |
| infer | 类型解构 | 捕获子类型 |
模块、命名空间与声明文件
TypeScript 的模块系统建立在 ES Module 之上,并额外提供了命名空间、`declare`、`.d.ts` 声明文件和三斜线指令来描述"没有类型的 JS"。
ES 模块与 import type
// 命名导出与导入
export interface User { id: number; name: string; }
export function createUser(name: string): User {
return { id: Date.now(), name };
}
export const VERSION = '1.0.0';
// 默认导出
export default class ApiClient { /* ... */ }
// 仅导入类型(编译后完全擦除,不产生 import 语句)
import type { User } from './user';
// 混合导入,用 inline type 标记类型部分
import { createUser, type User as U } from './user';为什么用 `import type`:明确标记"这个导入只用于类型",编译后会被彻底删除,避免因副作用导入引入不必要的运行时依赖,也能规避 `isolatedModules` 下的循环依赖问题。
命名空间(namespace)
概念:命名空间是 TypeScript 早期的模块化方案,把相关类型/值组织在一个全局对象下。现代项目基本被 ES 模块取代,但在编写全局类型声明时仍有用武之地。
namespace Validation {
export interface Validator {
isValid(s: string): boolean;
}
export class EmailValidator implements Validator {
isValid(s: string) { return /@/.test(s); }
}
}
const v = new Validation.EmailValidator();namespace vs module 对比:
| 维度 | namespace | ES module |
| --- | --- | --- |
| 加载方式 | 全局合并 | 按文件按需 |
| Tree-shaking | 差 | 好 |
| 现代推荐 | 仅用于全局声明 | 首选 |
| 编译产物 | 命名空间对象 | import/export |
declare 与 .d.ts 声明文件
概念:`declare` 声明"某个东西在别处存在",只描述类型不生成实现。`.d.ts` 文件是纯类型声明文件,用来给 JS 库补类型,或声明全局变量。
// global.d.ts —— 声明全局变量与模块
declare const __APP_VERSION__: string; // 构建时注入的全局常量
declare function gtag(...args: any[]): void; // 第三方脚本注入的全局函数
// 为没有类型的 JS 模块补声明
declare module 'legacy-lib' {
export function doSomething(input: string): number;
export const config: { debug: boolean };
}
// 为非 JS 资源声明模块(如 import 图片/样式)
declare module '*.svg' {
const content: string;
export default content;
}
declare module '*.module.css' {
const classes: { readonly [key: string]: string };
export default classes;
}
// 扩展全局接口
declare global {
interface Window {
dataLayer: unknown[];
}
}
export {}; // 让本文件成为模块,declare global 才生效三斜线指令与类型声明发布
// 三斜线指令:引用其他声明文件或库类型(多见于 .d.ts)
/// <reference types="node" />
/// <reference path="./other.d.ts" />类型声明发布方式:
| 方式 | 说明 | 适用 |
| --- | --- | --- |
| 库内置 types | package.json 的 "types" 字段指向 .d.ts | 库作者自带类型 |
| @types/xxx | DefinitelyTyped 社区维护 | 第三方 JS 库 |
| 本地 d.ts | 项目内补声明 | 私有/无类型库 |
常见坑:
tsconfig 关键选项详解
`tsconfig.json` 控制编译器行为。理解关键选项能避免大量"为什么没报错/为什么报错"的困惑。
strict 家族
`strict: true` 是一个开关,一次性打开下面一整组严格检查:
| 选项 | 作用 | 关掉的风险 |
| --- | --- | --- |
| strictNullChecks | null/undefined 需显式处理 | 空值访问崩溃 |
| strictFunctionTypes | 函数参数逆变检查 | 回调类型不安全 |
| strictBindCallApply | bind/call/apply 参数检查 | 调用参数错配 |
| strictPropertyInitialization | 类属性必须初始化 | 未初始化属性 |
| noImplicitAny | 禁止隐式 any | 类型悄悄丢失 |
| noImplicitThis | 禁止隐式 any 的 this | this 指向错乱 |
| alwaysStrict | 输出 "use strict" | 非严格模式坑 |
| useUnknownInCatchVariables | catch 变量为 unknown | 错误类型误判 |
// strictNullChecks 开启后的差异
function getLength(s: string | null) {
// return s.length; // Error: s 可能为 null
return s?.length ?? 0; // 正确处理
}模块解析与路径
// tsconfig.json 关键片段(示意)
// {
// "compilerOptions": {
// "target": "ES2020", // 编译目标 JS 版本
// "module": "ESNext", // 输出模块格式
// "moduleResolution": "Bundler", // 模块解析策略
// "baseUrl": ".",
// "paths": { "@/*": ["src/*"] }, // 路径别名
// "lib": ["ES2020", "DOM"], // 可用的内置类型库
// "esModuleInterop": true, // CJS/ESM 互操作
// "skipLibCheck": true, // 跳过 .d.ts 检查(提速)
// "resolveJsonModule": true // 允许 import JSON
// }
// }重要选项说明:
| 选项 | 常用值 | 作用 |
| --- | --- | --- |
| target | ES2020/ESNext | 编译到的语法版本 |
| module | ESNext/CommonJS | 输出模块系统 |
| moduleResolution | Bundler/Node16 | 如何找模块 |
| paths + baseUrl | @/ → src/ | 路径别名 |
| lib | DOM/ES2020 | 内置 API 类型 |
| esModuleInterop | true | 允许 default 导入 CJS |
| skipLibCheck | true | 跳过依赖类型检查,编译提速可达 30% |
| declaration | true | 生成 .d.ts(库作者需要) |
| noEmit | true | 只类型检查不产出(配合 Babel/esbuild) |
常见坑:`paths` 只影响类型检查,不改变运行时解析——运行时还需打包器(Vite/Webpack)或 `tsconfig-paths` 配套映射,否则运行时找不到模块。
最佳实践:新项目直接 `strict: true`;`skipLibCheck: true` 提速;用 Vite/esbuild 时 `noEmit: true` + `isolatedModules: true`。
装饰器与框架实战类型
实验性装饰器 vs TC39 新标准
概念:装饰器是加在类、方法、属性上的特殊声明。TypeScript 早期实现的是"实验性装饰器"(需 `experimentalDecorators`),5.0 起支持 TC39 标准装饰器,二者签名不同。
// TC39 标准装饰器(TS 5.0+,无需 experimentalDecorators)
function logged<This, Args extends any[], Return>(
target: (this: This, ...args: Args) => Return,
context: ClassMethodDecoratorContext
) {
const name = String(context.name);
return function (this: This, ...args: Args): Return {
console.log(`调用 ${name},参数:`, args);
return target.call(this, ...args);
};
}
class Calculator {
@logged
add(a: number, b: number) { return a + b; }
}两种装饰器对比:
| 维度 | 实验性装饰器 | TC39 标准装饰器 |
| --- | --- | --- |
| 配置 | experimentalDecorators | 5.0+ 默认 |
| 参数签名 | target, key, descriptor | value, context |
| 参数装饰器 | 支持 | 尚不支持 |
| metadata | 配 reflect-metadata | context.metadata |
| 生态(NestJS等) | 大量依赖 | 逐步迁移 |
与 React 结合的类型
import { useState, type ReactNode, type FC } from 'react';
// 组件 props 类型
interface CardProps {
title: string;
children: ReactNode; // 可渲染内容
onClose?: () => void; // 可选回调
}
const Card: FC<CardProps> = ({ title, children, onClose }) => {
const [open, setOpen] = useState<boolean>(true); // 泛型指定 state 类型
if (!open) return null;
return null; // 示意
};
// 事件类型
function handleChange(e: React.ChangeEvent<HTMLInputElement>) {
console.log(e.target.value);
}
// 泛型组件
interface ListProps<T> {
items: T[];
renderItem: (item: T) => ReactNode;
}
function List<T>({ items, renderItem }: ListProps<T>) {
return items.map(renderItem);
}与 Vue 结合的类型
import { defineComponent, ref, type PropType } from 'vue';
interface Todo { id: number; text: string; done: boolean; }
export default defineComponent({
props: {
// 用 PropType 标注复杂类型
todos: { type: Array as PropType<Todo[]>, required: true },
title: { type: String, default: '待办' },
},
setup(props) {
const count = ref<number>(0); // ref 泛型
const addCount = () => { count.value++; };
return { count, addCount };
},
});类型兼容性、协变与逆变
概念:TypeScript 用结构化类型判断兼容性。协变(Covariance)指"子类型关系随成员方向保持一致",逆变(Contravariance)指"方向相反",主要体现在函数参数上。
// 结构兼容:只要结构满足即可赋值
interface Point2D { x: number; y: number; }
interface Point3D { x: number; y: number; z: number; }
let p2: Point2D;
let p3: Point3D = { x: 1, y: 2, z: 3 };
p2 = p3; // OK:Point3D 结构上包含 Point2D(多的属性无妨)
// p3 = p2; // Error:缺少 z
// 协变:返回值、数组元素
type Animal = { name: string };
type Dog = { name: string; bark: () => void };
let animals: Animal[];
let dogs: Dog[] = [{ name: 'a', bark: () => {} }];
animals = dogs; // 协变:Dog[] 可赋给 Animal[]
// 逆变:函数参数(strictFunctionTypes 下)
type AnimalHandler = (a: Animal) => void;
type DogHandler = (d: Dog) => void;
let handleAnimal: AnimalHandler = (a) => console.log(a.name);
let handleDog: DogHandler = handleAnimal; // OK:参数逆变,接受更宽的处理器
// handleAnimal = handleDog; // Error:handleDog 需要 bark,但传入的可能只是 Animal协变/逆变速记表:
| 位置 | 型变 | 记忆 |
| --- | --- | --- |
| 函数返回值 | 协变 | 返回更具体 OK |
| 函数参数 | 逆变 | 接受更宽泛 OK |
| 数组元素(读) | 协变 | Dog[] → Animal[] |
| 可变数组(写) | 本应不变 | TS 放宽为协变(有坑) |
| 方法参数 | 双变 | TS 为易用性放宽 |
常见坑:
最佳实践:回调类型用函数属性写法而非方法写法以获得更严格检查;只读数组用 `readonly T[]` 避免协变写入坑;始终开 `strict`。
常见坑与最佳实践总览
| 坑 | 后果 | 对策 |
| --- | --- | --- |
| 滥用 any | 类型安全全失 | 换 unknown + 收窄 |
| 到处 as 断言 | 骗过编译器埋运行时坑 | 用类型守卫 |
| let 字面量宽化 | 类型意外变宽 | as const / 显式标注 |
| 数字枚举可赋任意数 | 越界值不报错 | 用字符串枚举/字面量联合 |
| Partial 只浅层 | 嵌套未变可选 | 自写 DeepPartial |
| paths 不影响运行时 | 打包后找不到模块 | 配打包器别名 |
| 忘 never 穷尽 | 新增分支漏处理 | default 赋值 never |
| 递归类型超深度 | 编译报错 | 改尾递归 |
小结表
| 主题 | 一句话 | 关键词 |
| --- | --- | --- |
| 模块 | ES Module 为主 | import type |
| 声明文件 | 给 JS 补类型 | declare、.d.ts |
| tsconfig | 控制编译行为 | strict、paths |
| 装饰器 | 元编程注解 | TC39 标准 |
| 框架类型 | props/ref/事件 | FC、PropType |
| 型变 | 结构兼容规则 | 协变、逆变 |
类型安全实战模式
前面的章节讲的是"零件",本节把它们拼成几个高频实战模式,直接可用于业务代码。
类型安全的 API 层
场景:前后端约定接口,用类型描述请求与响应,配合泛型封装 fetch,让每个接口调用都有精确的返回类型。
// 统一响应包装
interface ApiResult<T> {
code: number;
data: T;
message: string;
}
// 定义接口映射:路径 -> 响应数据类型
interface ApiMap {
'/user/profile': { id: number; name: string; avatar: string };
'/order/list': { orders: Array<{ id: string; total: number }> };
}
// 泛型请求函数:传入 path,自动推断返回类型
async function request<P extends keyof ApiMap>(
path: P
): Promise<ApiResult<ApiMap[P]>> {
const res = await fetch(path);
if (!res.ok) throw new Error(`HTTP ${res.status}`);
return res.json();
}
// 使用:返回类型被精确推断,无需手动断言
async function demo() {
const profile = await request('/user/profile');
console.log(profile.data.name); // string,IDE 有补全
const orders = await request('/order/list');
console.log(orders.data.orders[0].total); // number
}类型安全的事件系统
场景:一个发布订阅中心,不同事件携带不同 payload,用映射类型保证 emit 和 on 的 payload 类型对齐。
// 事件名 -> payload 类型
interface EventPayloads {
login: { userId: number };
logout: undefined;
message: { from: string; text: string };
}
class TypedEmitter {
private handlers: {
[K in keyof EventPayloads]?: Array<(payload: EventPayloads[K]) => void>;
} = {};
on<K extends keyof EventPayloads>(
event: K,
handler: (payload: EventPayloads[K]) => void
): void {
(this.handlers[event] ??= []).push(handler as any);
}
emit<K extends keyof EventPayloads>(event: K, payload: EventPayloads[K]): void {
this.handlers[event]?.forEach(h => h(payload));
}
}
const bus = new TypedEmitter();
bus.on('login', p => console.log(p.userId)); // p 类型精确
bus.emit('login', { userId: 1 }); // payload 校验
// bus.emit('login', { userId: 'x' }); // Error:userId 应为 number用 satisfies 兼顾校验与推断(TS 4.9+)
概念:`satisfies` 检查表达式是否满足某类型,但不改变表达式本身被推断出的更窄类型。它解决了"用类型标注会丢失字面量精度、不标注又没校验"的两难。
type RGB = [number, number, number];
type Palette = Record<string, RGB | string>;
// 用 : Palette 标注:green 会被拓宽成 RGB | string,丢失元组精度
const p1: Palette = { red: [255, 0, 0], green: '#00ff00' };
// p1.red 类型是 RGB | string,无法直接当元组用
// 用 satisfies:既校验满足 Palette,又保留每个值的精确类型
const p2 = {
red: [255, 0, 0],
green: '#00ff00',
} satisfies Palette;
p2.red[0]; // number,保留了元组类型
p2.green.toUpperCase(); // string,保留了字符串类型satisfies vs as vs 类型标注对比:
| 方式 | 是否校验 | 是否保留窄类型 | 风险 |
| --- | --- | --- | --- |
| : Type(标注) | 是 | 否(拓宽) | 丢精度 |
| as Type(断言) | 否(强转) | 部分 | 骗编译器 |
| satisfies Type | 是 | 是 | 无 |
品牌类型(Branded Types)防混淆
场景:`userId` 和 `orderId` 都是 `string`,结构上完全兼容,容易传错。用品牌类型给它们打上"标记",编译期区分。
// 交叉一个带唯一 tag 的类型,制造名义上的区别
type Brand<T, B extends string> = T & { readonly __brand: B };
type UserId = Brand<string, 'UserId'>;
type OrderId = Brand<string, 'OrderId'>;
function asUserId(id: string): UserId { return id as UserId; }
function getUser(id: UserId) { /* ... */ }
const uid = asUserId('u_123');
getUser(uid); // OK
// getUser('u_123'); // Error:普通 string 不能当 UserId
// getUser(orderId); // Error:OrderId 不能当 UserId常见坑:
最佳实践:API 层用泛型 + 接口映射;同型不同义的值用品牌类型;配置对象用 `satisfies` 保精度又校验。
小结表
| 模式 | 解决什么 | 关键技术 |
| --- | --- | --- |
| 泛型 API 层 | 接口返回类型精确 | keyof 映射 + 泛型 |
| 类型化事件系统 | payload 对齐 | 映射类型 |
| satisfies | 校验又保精度 | 4.9+ |
| 品牌类型 | 同型防混淆 | 交叉 tag |
总结
| 概念 | 一句话记忆 | 关键词 |
| --- | --- | --- |
| 超集 | 合法 JS 就是合法 TS | 类型擦除 |
| 类型推断 | 能不写就不写 | 初始值、上下文 |
| 类型断言 | "我比编译器懂",慎用 | as、!、as const |
| any/unknown | unknown 是安全版 any | 收窄 |
| interface vs type | 对象用 interface,联合用 type | extends、& |
| 泛型 | 类型的参数化 | T、extends、keyof |
| 工具类型 | 从已有类型派生新类型 | Partial、Pick、Omit、Record |
| 类型守卫 | 联合类型的收窄 | typeof、in、is、never |
| 高级类型 | 类型层面编程 | 条件、映射、模板字面量 |
| strict 模式 | 严格才安全 | strictNullChecks |
一句话:TypeScript 的价值不在于"写更多类型",而在于用尽量少的标注,换取编译器帮你在写代码时就发现错误。