TypeScript 教程:面向前端 JavaScript 开发者的实战指南
本文档系统、全面地介绍 TypeScript,聚焦前端开发场景(React / Vue 3),覆盖从入门到进阶的完整学习路径。所有示例均来自真实前端项目场景。
目录
- 第一章 为什么前端开发者需要 TypeScript
- 第二章 基础类型系统
- 第三章 接口与类型别名
- 第四章 泛型在前端中的实际应用
- 第五章 类型守卫与类型收窄
- 第六章 工具类型(Utility Types)实战
- 第七章 React 与 Vue 中的 TypeScript 最佳实践
- 第八章 类型声明文件与第三方库类型处理
- 第九章 tsconfig.json 配置详解
- 第十章 从 JavaScript 项目迁移到 TypeScript
- 第十一章 常见陷阱与调试技巧
1.1 JavaScript 的"自由"代价
JavaScript 的动态类型是它快速上手、灵活表达的优势,但在项目规模增长时会暴露出三类典型问题:
问题一:运行时才发现类型错误
// 这段代码在编码时没有任何报错提示
function formatPrice(price, currency) {
return `${currency}${price.toFixed(2)}`;
}
// 两周后有人这样调用:
formatPrice("19.9", "¥");
// ❌ 运行时 TypeError: "19.9".toFixed is not a function
// 原因:传入了字符串而非数字,但 JS 不会在编码时提醒你
formatPrice(19.9, undefined);
// ❌ 结果: "undefined19.90" —— 静默产生 bug
在大型前端项目中,这类错误会被层层传递:API 返回的数据结构变了 → Redux/Pinia store 类型不对 → 组件渲染异常。定位链路长、排查成本高。
问题二:重构时缺乏安全网
// 一个被 30 个组件引用的工具函数
function getUserDisplayName(user) {
return `${user.first_name} ${user.last_name}`;
}
// 某次重构将字段名改为 camelCase:
// user.first_name → user.firstName
// user.last_name → user.lastName
// 改完之后——没有任何编译错误,但 30 个组件全部静默显示 "undefined undefined"
问题三:团队协作的认知成本
// 新人接手一段代码,只能靠注释和猜测理解数据结构:
function handleOrder(data, options) {
// data 是什么结构?options 有哪些可选字段?
// data.items 是数组吗?data.total 是数字还是字符串?
// 只能全局搜索调用处,逐行 console.log 来推测
}
TypeScript 的静态类型系统就是为解决这三个问题而生:让类型错误从运行时提前到编译时。
1.2 TypeScript 是什么
TypeScript 是 JavaScript 的类型超集(Typed Superset),这意味着:
- 所有合法的 JS 代码本身就是合法的 TS 代码(在默认配置下)
- TS 在 JS 之上增加了一层可选的类型注解系统
- TS 编译后输出的是纯净的 JavaScript,不引入任何运行时开销
// 这就是一段完全合法的 TypeScript
const message = "Hello World"; // 类型自动推断为 string
// 也可以显式添加类型注解
const price: number = 19.9;
const currency: string = "¥";
function formatPrice(price: number, currency: string): string {
return `${currency}${price.toFixed(2)}`;
}
// 现在传入错误参数会立即报编译错误
formatPrice("19.9", "¥");
// ❌ 编译错误: Argument of type 'string' is not assignable to parameter of type 'number'.
TypeScript 的类型系统仅在编译阶段工作。编译完成后,产物就是纯 JS:
.ts 文件 ──[tsc 编译]──> .js 文件 ──[浏览器/Node 执行]
↑ ↑
类型检查发生在这里 没有任何 TS 痕迹
1.3 TypeScript 为前端开发者带来的核心收益
1.3.1 智能提示与自动补全
编辑器(VS Code/WebStorm)基于 TS 类型信息提供精准的智能提示:
interface User {
id: number;
name: string;
email: string;
avatar: string;
}
function fetchUser(id: number): Promise<User> {
return fetch(`/api/users/${id}`).then(res => res.json());
}
const user = await fetchUser(1);
// 输入 "user." → 编辑器自动提示:id, name, email, avatar
// 无需记忆 API 返回字段,无需翻阅接口文档
1.3.2 重构安全网
// 修改 User 接口的字段名
interface User {
id: number;
displayName: string; // 把 name 改为 displayName
email: string;
}
// 所有引用 user.name 的地方立即飘红
// tsc --noEmit 可以一次性列出所有需要修改的位置
// 点击错误 → F2 重命名 → 全部修改完成,零遗漏
1.3.3 团队协作的自文档化
// 类型定义本身就是最好的文档
interface OrderResponse {
code: number;
data: {
orderId: string;
items: Array<{
productId: number;
productName: string;
quantity: number;
unitPrice: number;
}>;
totalAmount: number; // 单位:分
status: 'pending' | 'paid' | 'shipped' | 'completed' | 'cancelled';
createdAt: string; // ISO 8601
};
message: string;
}
新人看到这个 interface 就能理解 API 返回的完整数据结构,不需要问同事、不需要查文档。
1.3.4 框架层面的深度集成
React 和 Vue 都为 TypeScript 提供了一等公民支持:
// React:useState 自动推导类型
const [count, setCount] = useState(0);
// count: number, setCount: Dispatch<SetStateAction<number>>
// Vue 3:defineProps 配合类型
const props = defineProps<{ title: string; count: number }>();
// props.title 自动推导为 string
1.4 学习路径概览
对于有 JavaScript 基础的前端开发者,TS 学习可以分为四个阶段:
| 阶段 | 内容 | 预估时间 | 目标 |
|---|---|---|---|
| 入门 | 基础类型、interface/type、函数类型 | 1-2 天 | 能在已有 TS 项目中写业务代码 |
| 熟练 | 泛型、类型守卫、工具类型 | 1-2 周 | 能封装类型安全的工具函数和 Hook |
| 框架集成 | React/Vue TS 模式、声明文件 | 1 周 | 能在框架项目中独立做类型设计 |
| 工程化 | tsconfig 调优、迁移策略、类型体操 | 持续积累 | 能主导项目的 TS 架构和迁移 |
TS 学习曲线并非线性——前 20% 的知识覆盖 80% 的日常场景。基础类型 + interface + 泛型基础就能覆盖绝大多数业务代码需求。本教程按此路径组织,从第 2 章的基础类型开始。
2.1 基础类型:从 JavaScript 到 TypeScript
TypeScript 继承了 JavaScript 的所有基础类型,并增加了几个关键补充。下表展示了 JS 类型与 TS 类型的对应关系:
| JavaScript 类型 | TypeScript 类型 | 示例 | 前端常见场景 |
|---|---|---|---|
string | string | "hello" | 用户输入、API 返回的文本、DOM 文本内容 |
number | number | 42, 3.14, NaN | 价格、数量、坐标、尺寸、百分比 |
boolean | boolean | true, false | 开关状态、条件判断、加载状态 |
Array | T[] 或 Array<T> | [1, 2, 3] | 列表数据、选项数组、搜索结果 |
Object | object | { x: 1, y: 2 } | 配置对象、表单数据、API 响应 |
null | null | null | 显式清空值 |
undefined | undefined | undefined | 未初始化变量、可选参数 |
Function | (args) => returnType | (x: number) => x * 2 | 事件处理器、回调函数 |
Symbol | symbol | Symbol("key") | 唯一标识符、私有属性键 |
2.1.1 类型注解语法
// 变量声明
const name: string = "张三";
const age: number = 25;
const isActive: boolean = true;
// 数组
const numbers: number[] = [1, 2, 3];
const strings: Array<string> = ["a", "b", "c"]; // 两种写法等价
// 对象
const user: { name: string; age: number } = {
name: "张三",
age: 25
};
// 函数参数与返回值
function greet(name: string): string {
return `Hello, ${name}!`;
}
2.1.2 类型推断:让 TS 为你工作
TypeScript 有强大的类型推断能力,大部分情况下你不需要显式写类型:
// 变量声明时自动推断
const message = "Hello World"; // 推断为 string
const count = 42; // 推断为 number
const isLoading = false; // 推断为 boolean
const items = [1, 2, 3]; // 推断为 number[]
// 函数返回值自动推断
function add(a: number, b: number) {
return a + b; // 返回值自动推断为 number
}
// 对象字面量自动推断
const user = {
name: "张三",
age: 25
}; // 推断为 { name: string; age: number }
最佳实践:让 TypeScript 尽可能推断,只在以下情况显式注解:
- 函数参数(必须)
- 对象字面量需要更精确的类型时
- 变量声明与初始化分离时
- 需要明确表达意图时
2.2 元组与枚举:前端状态管理利器
2.2.1 元组(Tuple):固定长度和类型的数组
元组在前端中常用于表示坐标、尺寸、颜色值等固定结构的数据:
// 坐标点
const point: [number, number] = [10, 20];
// 错误:point[0] = "10"; // ❌ 不能将字符串赋值给 number
// RGB 颜色
const color: [number, number, number] = [255, 128, 64];
// React useState 返回的元组
const [count, setCount] = useState<number>(0);
// count: number, setCount: React.Dispatch<React.SetStateAction<number>>
// 带标签的元组(TypeScript 4.0+)
const user: [name: string, age: number, isAdmin: boolean] =
["张三", 25, false];
2.2.2 枚举(Enum):定义命名常量集合
枚举在前端中常用于状态码、权限级别、菜单类型等场景:
// 数字枚举(默认从 0 开始)
enum OrderStatus {
Pending, // 0
Paid, // 1
Shipped, // 2
Completed, // 3
Cancelled // 4
}
const order = { status: OrderStatus.Paid };
// 字符串枚举(更安全,推荐)
enum ButtonVariant {
Primary = "primary",
Secondary = "secondary",
Danger = "danger"
}
// 常量枚举(编译时内联,无运行时开销)
const enum MediaType {
Image = "image",
Video = "video",
Audio = "audio"
}
2.3 any、unknown、never:特殊类型
2.3.1 any:类型系统的逃生舱
any 表示"任意类型",关闭了该变量的所有类型检查:
let value: any = "hello";
value = 42; // ✅ 可以
value = true; // ✅ 可以
value.toFixed(2); // ✅ 编译通过,但运行时可能报错
使用原则:尽量避免。仅在以下场景临时使用:
- 迁移旧 JS 代码时的过渡期
- 处理第三方库的无类型返回值
- 快速原型阶段
2.3.2 unknown:更安全的 any
unknown 表示"未知类型",比 any 更安全——必须经过类型检查后才能使用:
let value: unknown = "hello";
// value.toUpperCase(); // ❌ 编译错误:Object is of type 'unknown'
// 必须先用类型守卫缩小范围
if (typeof value === "string") {
value.toUpperCase(); // ✅ 现在安全了
}
// 或者用类型断言
(value as string).toUpperCase();
最佳实践:优先用 unknown 替代 any,强制开发者做类型检查。
2.3.3 never:永不存在的值
never 表示永远不会发生的类型,常用于错误处理、无限循环、穷尽性检查:
// 总是抛出错误的函数
function throwError(message: string): never {
throw new Error(message);
}
// 无限循环
function infiniteLoop(): never {
while (true) {
// ...
}
}
// 穷尽性检查(Discriminated Unions)
type Shape =
| { kind: "circle"; radius: number }
| { kind: "square"; size: number }
| { kind: "triangle"; base: number; height: number };
function getArea(shape: Shape): number {
switch (shape.kind) {
case "circle": return Math.PI * shape.radius ** 2;
case "square": return shape.size ** 2;
case "triangle": return (shape.base * shape.height) / 2;
default:
// 如果新增了 shape 类型,这里会报编译错误
const _exhaustiveCheck: never = shape;
return _exhaustiveCheck;
}
}
2.4 联合类型与交叉类型
2.4.1 联合类型(Union Types):表示"或"的关系
联合类型在前端中最常用于API 响应、表单字段、组件 Props:
// 用户 ID 可以是数字或字符串
type UserId = number | string;
// API 响应:成功或失败
type ApiResponse<T> =
| { success: true; data: T }
| { success: false; error: string };
// 表单字段:文本、数字、日期
type FormField = string | number | Date;
// 组件 Props 的 size 属性
type ButtonSize = "small" | "medium" | "large";
// 处理联合类型需要类型守卫(第 5 章详解)
function handleResponse(response: ApiResponse<User>) {
if (response.success) {
console.log(response.data); // 这里 response 被收窄为成功类型
} else {
console.error(response.error); // 这里 response 被收窄为失败类型
}
}
2.4.2 交叉类型(Intersection Types):表示"与"的关系
交叉类型在前端中常用于组合多个类型、扩展接口:
// 基础用户类型
interface User {
id: number;
name: string;
}
// 用户详情
interface UserDetail {
email: string;
phone: string;
}
// 组合成完整的用户信息
type FullUser = User & UserDetail;
// 等价于 { id: number; name: string; email: string; phone: string }
// 扩展组件 Props
interface BaseProps {
className?: string;
style?: React.CSSProperties;
}
interface ButtonProps {
onClick: () => void;
children: React.ReactNode;
}
type FullButtonProps = BaseProps & ButtonProps;
// 与联合类型结合:条件渲染
type ComponentProps =
| { type: "button"; onClick: () => void }
| { type: "link"; href: string }
| { type: "text"; content: string };
2.5 字面量类型与类型推断进阶
2.5.1 字面量类型(Literal Types)
字面量类型允许你指定变量必须是某个具体的值:
// 字符串字面量类型
type Direction = "up" | "down" | "left" | "right";
const direction: Direction = "up"; // ✅
// const direction: Direction = "north"; // ❌
// 数字字面量类型
type StatusCode = 200 | 301 | 404 | 500;
const status: StatusCode = 200; // ✅
// 布尔字面量类型
type Truthy = true;
const isTruthy: Truthy = true; // ✅
// const isTruthy: Truthy = false; // ❌
2.5.2 const 断言与只读数组
const 断言让 TypeScript 将表达式推断为最具体的类型:
// 普通数组推断
const numbers = [1, 2, 3]; // 推断为 number[]
// const 断言
const numbersAsConst = [1, 2, 3] as const;
// 推断为 readonly [1, 2, 3] 元组
// 对象字面量
const user = {
name: "张三",
age: 25
} as const;
// 推断为 { readonly name: "张三"; readonly age: 25 }
// 只读数组和元组
const readonlyNumbers: readonly number[] = [1, 2, 3];
// readonlyNumbers.push(4); // ❌ 编译错误
const readonlyTuple: readonly [number, string] = [1, "hello"];
// readonlyTuple[0] = 2; // ❌ 编译错误
2.6 类型断言:告诉编译器"我知道我在做什么"
类型断言在前端中常用于DOM 操作、第三方库调用、类型转换:
// 两种语法
const value1 = someValue as string; // 推荐
const value2 = <string>someValue; // JSX 中会冲突,不推荐
// DOM 元素类型断言
const input = document.getElementById("username") as HTMLInputElement;
input.value = "张三"; // ✅ 现在知道是 input 元素
// 非空断言(!)
const element = document.getElementById("app")!; // 告诉 TS 这个元素一定存在
element.classList.add("loaded");
// 双重断言(谨慎使用)
const value = someValue as unknown as string; // 先转 unknown 再转目标类型
最佳实践:
- 优先使用
as语法 - 非空断言
!只在确定不为 null/undefined 时使用 - 双重断言是最后的逃生舱,尽量用类型守卫替代
2.7 前端实战:表单数据建模
综合运用基础类型,为前端表单建模:
// 表单字段类型
type FieldType = "text" | "number" | "email" | "password" | "date" | "select";
// 表单字段配置
interface FieldConfig {
name: string;
label: string;
type: FieldType;
required?: boolean;
placeholder?: string;
options?: Array<{ label: string; value: string }>;
validation?: {
min?: number;
max?: number;
pattern?: RegExp;
custom?: (value: any) => string | null;
};
}
// 表单数据(联合类型处理多态字段)
type FormData = {
[key: string]: string | number | boolean | Date | string[];
};
// 表单状态
type FormStatus = "idle" | "validating" | "submitting" | "success" | "error";
// 完整的表单 Hook 类型
interface UseFormReturn<T extends FormData> {
data: T;
status: FormStatus;
errors: Record<keyof T, string | null>;
setField: (name: keyof T, value: T[keyof T]) => void;
validate: () => boolean;
submit: () => Promise<void>;
reset: () => void;
}
这个类型设计覆盖了前端表单的常见需求,为后续实现类型安全的表单 Hook 打下基础。
3.1 interface:定义对象的结构契约
interface 是 TypeScript 最核心的类型定义方式之一,用于描述对象"应该长什么样":
// 基本用法:定义 API 响应结构
interface User {
id: number;
name: string;
email: string;
isAdmin: boolean;
}
// 使用 interface
const user: User = {
id: 1,
name: "张三",
email: "zhangsan@example.com",
isAdmin: false
};
// 函数参数使用 interface
function renderUser(user: User): string {
return `<div>${user.name} (${user.email})</div>`;
}
3.1.1 可选属性与只读属性
interface Product {
readonly id: number; // 只读:创建后不可修改
name: string;
price: number;
description?: string; // 可选:可以不存在
tags?: string[]; // 可选数组
metadata?: Record<string, string>; // 可选的对象映射
}
const product: Product = {
id: 1,
name: "TypeScript 教程",
price: 49.9
};
// product.id = 2; // ❌ 编译错误:id 是只读属性
product.name = "TypeScript 深入教程"; // ✅ 非只读属性可以修改
3.1.2 索引签名:动态属性名
当对象的属性名不确定时(如字典、配置映射),使用索引签名:
// 字符串索引签名:所有属性值必须是 string
interface StringDict {
[key: string]: string;
}
const translations: StringDict = {
"hello": "你好",
"goodbye": "再见",
"submit": "提交"
};
// 数字索引签名:类数组对象
interface StringArray {
[index: number]: string;
}
const arr: StringArray = ["a", "b", "c"];
const firstItem: string = arr[0];
// 混合索引签名
interface ConfigStore {
[key: string]: string | number | boolean; // 字符串索引
version: number; // 必须兼容索引签名类型
env: string;
}
3.1.3 函数类型接口
// 接口描述函数签名
interface EventHandler {
(event: Event): void;
}
const handleClick: EventHandler = (event) => {
console.log("clicked", event.target);
};
// 接口描述构造函数
interface UserConstructor {
new (name: string, email: string): User;
}
// 接口同时描述属性和调用签名(如 jQuery 风格)
interface JQueryStyle {
(selector: string): HTMLElement | null;
version: string;
ready(callback: () => void): void;
}
3.1.4 接口继承
interface BaseEntity {
id: number;
createdAt: string;
updatedAt: string;
}
interface User extends BaseEntity {
name: string;
email: string;
}
interface Admin extends User {
permissions: string[];
role: "admin" | "super_admin";
}
// 多重继承
interface Readable {
read(): string;
}
interface Writable {
write(data: string): void;
}
interface FileHandle extends Readable, Writable {
path: string;
size: number;
}
3.2 type:类型别名的灵活表达
type 可以为任意类型创建别名,比 interface 更灵活:
// 基础别名
type ID = number | string;
type Point = { x: number; y: number };
type Callback = (result: any) => void;
// 联合类型别名(interface 做不到)
type Status = "idle" | "loading" | "success" | "error";
// 交叉类型(与 interface extends 类似)
type Named = { name: string };
type Aged = { age: number };
type Person = Named & Aged;
// 元组别名
type RGBA = [number, number, number, number];
type LatLng = [latitude: number, longitude: number];
3.2.1 type 的映射能力(interface 做不到)
// 将联合类型的每个成员映射为属性
type EventNames = "click" | "focus" | "blur" | "change";
type EventHandlers = {
[K in EventNames]: (event: Event) => void;
};
// 等价于:
// {
// click: (event: Event) => void;
// focus: (event: Event) => void;
// blur: (event: Event) => void;
// change: (event: Event) => void;
// }
// 条件类型
type IsString<T> = T extends string ? true : false;
type A = IsString<"hello">; // true
type B = IsString<number>; // false
3.3 interface vs type:选择指南
这是 TypeScript 社区最常被问的问题之一。两者的核心差异:
| 特性 | interface | type |
|---|---|---|
| 声明合并(同名自动合并) | ✅ 支持 | ❌ 不支持 |
| 继承/扩展 | extends | &(交叉类型) |
| 描述函数 | ✅ | ✅ |
| 描述数组/元组 | ⚠️ 可但不自然 | ✅ |
| 联合类型 | ❌ 不支持 | ✅ |
| 映射类型 | ❌ 不支持 | ✅ |
| 条件类型 | ❌ 不支持 | ✅ |
| 工具类型 | ❌ | ✅ |
3.3.1 声明合并:interface 的独家能力
// interface 同名会自动合并
interface Window {
title: string;
}
interface Window {
myLib: { version: string };
}
// 最终 Window 类型同时拥有 title 和 myLib
// 这常用于为第三方类型打补丁
// type 同名会报错
// type Window = { title: string }; // ❌ Duplicate identifier
3.3.2 前端项目中的选择原则
使用 interface 的场景:
├── 描述 API 响应的数据结构
├── 定义组件 Props(React/Vue 都推荐)
├── 面向对象的类实现
├── 需要被 extends 或 implements 的类型
└── 需要声明合并的场景(如全局类型扩展)
使用 type 的场景:
├── 联合类型(string | number)
├── 工具类型映射(Partial<T>、Pick<T, K> 等)
├── 元组类型
├── 条件类型
└── 函数类型的简写别名
3.4 前端实战场景
3.4.1 API 响应层类型设计
// 通用 API 响应包装
interface ApiResponse<T> {
code: number;
message: string;
data: T;
timestamp: number;
}
// 带分页的列表响应
interface PaginatedResponse<T> extends ApiResponse<T[]> {
pagination: {
page: number;
pageSize: number;
total: number;
totalPages: number;
};
}
// 具体业务类型
interface UserInfo {
id: number;
name: string;
email: string;
avatar: string;
}
// 使用
type UserListResponse = PaginatedResponse<UserInfo>;
type UserDetailResponse = ApiResponse<UserInfo>;
3.4.2 组件 Props 类型设计
// React 组件 Props
interface ButtonProps {
variant: "primary" | "secondary" | "danger" | "ghost";
size?: "small" | "medium" | "large";
disabled?: boolean;
loading?: boolean;
onClick?: () => void;
children: React.ReactNode;
}
// Vue 3 组件 Props(配合 defineProps)
interface ModalProps {
visible: boolean;
title: string;
width?: number | string;
closable?: boolean;
maskClosable?: boolean;
onClose?: () => void;
onConfirm?: () => void;
}
3.4.3 Redux / Pinia Store 类型
// Redux Toolkit Slice State
interface CounterState {
value: number;
status: "idle" | "loading" | "failed";
lastUpdated: string | null;
}
// Pinia Store
interface CartState {
items: Array<{
productId: number;
quantity: number;
price: number;
}>;
couponCode: string | null;
discountAmount: number;
}
3.4.4 路由配置类型
// 路由配置(React Router / Vue Router)
interface RouteConfig {
path: string;
name?: string;
component: React.ComponentType<any>; // 或 Vue defineComponent
meta?: {
title?: string;
requiresAuth?: boolean;
permissions?: string[];
keepAlive?: boolean;
};
children?: RouteConfig[];
}
// 带类型参数的路由
interface RouteParams {
userId: string;
postId?: string;
}
4.1 泛型基础:类型参数化
泛型的核心思想是让类型也成为参数,在定义时不指定具体类型,在使用时才确定。这就像函数的参数一样:
function identity<T>(arg: T): T {
return arg;
}
const num = identity<number>(42); // T = number
const str = identity("hello"); // T = string(自动推断)
4.1.1 泛型函数:前端工具函数
function getFirst<T>(arr: T[]): T | undefined {
return arr[0];
}
function getProperty<T, K extends keyof T>(obj: T, key: K): T[K] {
return obj[key];
}
const user = { name: "张三", age: 25 };
const name = getProperty(user, "name"); // string
const age = getProperty(user, "age"); // number
4.1.2 泛型接口
interface ApiResponse<T> {
code: number;
message: string;
data: T;
}
type UserResponse = ApiResponse<{ id: number; name: string }>;
type ProductListResponse = ApiResponse<Array<{ id: number; price: number }>>;
// 泛型默认值
interface Paginated<T = any> {
items: T[];
total: number;
page: number;
pageSize: number;
}
4.1.3 泛型类
class Cache<T> {
private data: Map<string, T> = new Map();
set(key: string, value: T): void { this.data.set(key, value); }
get(key: string): T | undefined { return this.data.get(key); }
}
const userCache = new Cache<{ id: number; name: string }>();
const configCache = new Cache<string>();
4.2 泛型约束
// T 必须有 length 属性
function logLength<T extends { length: number }>(arg: T): void {
console.log(arg.length);
}
logLength([1, 2, 3]); // ✅
logLength("hello"); // ✅
// logLength(42); // ❌
// K 必须是 T 的键
function pick<T, K extends keyof T>(obj: T, keys: K[]): Pick<T, K> {
const result = {} as Pick<T, K>;
keys.forEach(key => { result[key] = obj[key]; });
return result;
}
const user = { id: 1, name: "张三", email: "zhangsan@example.com" };
const partial = pick(user, ["name", "email"]); // { name: string; email: string }
4.3 前端实战:useLocalStorage
import { useState } from "react";
function useLocalStorage<T>(key: string, initialValue: T): [T, (value: T) => void] {
const readValue = (): T => {
try {
const item = window.localStorage.getItem(key);
return item ? JSON.parse(item) : initialValue;
} catch {
return initialValue;
}
};
const [storedValue, setStoredValue] = useState<T>(readValue);
const setValue = (value: T) => {
try {
setStoredValue(value);
window.localStorage.setItem(key, JSON.stringify(value));
} catch (error) {
console.warn(`Error setting localStorage key "${key}":`, error);
}
};
return [storedValue, setValue];
}
const [theme, setTheme] = useLocalStorage<"light" | "dark">("theme", "light");
const [user, setUser] = useLocalStorage<{ name: string; age: number }>("user", { name: "", age: 0 });
4.4 前端实战:useRequest
import { useState, useEffect } from "react";
interface UseRequestOptions<T> {
url: string;
method?: "GET" | "POST" | "PUT" | "DELETE";
body?: any;
onSuccess?: (data: T) => void;
onError?: (error: Error) => void;
}
interface UseRequestResult<T> {
data: T | null;
loading: boolean;
error: Error | null;
refetch: () => Promise<void>;
}
function useRequest<T>(options: UseRequestOptions<T>): UseRequestResult<T> {
const [data, setData] = useState<T | null>(null);
const [loading, setLoading] = useState(false);
const [error, setError] = useState<Error | null>(null);
const fetchData = async () => {
setLoading(true);
setError(null);
try {
const response = await fetch(options.url, {
method: options.method || "GET",
headers: { "Content-Type": "application/json" },
body: options.body ? JSON.stringify(options.body) : undefined,
});
if (!response.ok) throw new Error(`HTTP ${response.status}`);
const result = await response.json();
setData(result);
options.onSuccess?.(result);
} catch (err) {
const error = err instanceof Error ? err : new Error(String(err));
setError(error);
options.onError?.(error);
} finally {
setLoading(false);
}
};
useEffect(() => { fetchData(); }, [options.url]);
return { data, loading, error, refetch: fetchData };
}
interface User { id: number; name: string; }
const { data: users, loading } = useRequest<User[]>({ url: "/api/users" });
4.5 前端实战:useForm
import { useState, useCallback } from "react";
interface UseFormOptions<T extends Record<string, any>> {
initialValues: T;
validate?: (values: T) => Partial<Record<keyof T, string>>;
onSubmit: (values: T) => void | Promise<void>;
}
interface UseFormReturn<T extends Record<string, any>> {
values: T;
errors: Partial<Record<keyof T, string>>;
isSubmitting: boolean;
handleChange: (name: keyof T, value: T[keyof T]) => void;
handleSubmit: (e: React.FormEvent) => Promise<void>;
reset: () => void;
}
function useForm<T extends Record<string, any>>(
options: UseFormOptions<T>
): UseFormReturn<T> {
const [values, setValues] = useState<T>(options.initialValues);
const [errors, setErrors] = useState<Partial<Record<keyof T, string>>>({});
const [isSubmitting, setIsSubmitting] = useState(false);
const handleChange = useCallback((name: keyof T, value: T[keyof T]) => {
setValues(prev => ({ ...prev, [name]: value }));
setErrors(prev => ({ ...prev, [name]: undefined }));
}, []);
const handleSubmit = useCallback(async (e: React.FormEvent) => {
e.preventDefault();
setIsSubmitting(true);
if (options.validate) {
const newErrors = options.validate(values);
setErrors(newErrors);
if (Object.keys(newErrors).length > 0) { setIsSubmitting(false); return; }
}
try { await options.onSubmit(values); }
finally { setIsSubmitting(false); }
}, [values, options]);
const reset = useCallback(() => {
setValues(options.initialValues);
setErrors({});
setIsSubmitting(false);
}, [options.initialValues]);
return { values, errors, isSubmitting, handleChange, handleSubmit, reset };
}
// 使用
interface LoginForm { email: string; password: string; rememberMe: boolean; }
const { values, handleChange, handleSubmit } = useForm<LoginForm>({
initialValues: { email: "", password: "", rememberMe: false },
validate: (values) => {
const errors: Partial<Record<keyof LoginForm, string>> = {};
if (!values.email.includes("@")) errors.email = "请输入有效邮箱";
if (values.password.length < 6) errors.password = "密码至少6位";
return errors;
},
onSubmit: async (values) => { await login(values); },
});
4.6 状态管理中的泛型
Redux Toolkit
import { createSlice, PayloadAction } from "@reduxjs/toolkit";
interface ResourceState<T> {
data: T | null;
loading: boolean;
error: string | null;
}
function createResourceSlice<T>(name: string, initialState: T) {
return createSlice({
name,
initialState: { data: initialState, loading: false, error: null } as ResourceState<T>,
reducers: {
setData: (state, action: PayloadAction<T>) => {
state.data = action.payload;
state.loading = false;
state.error = null;
},
setLoading: (state) => { state.loading = true; state.error = null; },
setError: (state, action: PayloadAction<string>) => {
state.loading = false;
state.error = action.payload;
},
},
});
}
interface User { id: number; name: string; }
const userSlice = createResourceSlice<User>("user", { id: 0, name: "" });
Pinia
import { defineStore } from "pinia";
function createGenericStore<T>(id: string, initialData: T) {
return defineStore(id, {
state: () => ({ data: initialData, loading: false, error: null as string | null }),
actions: {
async fetchData(url: string) {
this.loading = true;
try {
const res = await fetch(url);
this.data = await res.json() as T;
} catch (err) {
this.error = err instanceof Error ? err.message : String(err);
} finally {
this.loading = false;
}
},
},
});
}
interface Product { id: number; name: string; price: number; }
const useProductStore = createGenericStore<Product>("product", { id: 0, name: "", price: 0 });
4.7 泛型组件
interface ListProps<T> {
items: T[];
renderItem: (item: T, index: number) => React.ReactNode;
keyExtractor: (item: T) => string | number;
}
function List<T>({ items, renderItem, keyExtractor }: ListProps<T>) {
return (
<ul>
{items.map((item, index) => (
<li key={keyExtractor(item)}>{renderItem(item, index)}</li>
))}
</ul>
);
}
// Vue 3 泛型组件
// <script setup lang="ts" generic="T">
// defineProps<{ items: T[]; renderItem: (item: T) => string }>();
// </script>
总结:泛型是 TypeScript 进阶的核心能力。在前端项目中,掌握泛型意味着你能够封装出类型安全的工具函数、自定义 Hook 和通用组件,让类型信息在整个调用链中流动,而非在每个层级手动补类型注解。
5.1 为什么需要类型守卫
当 TypeScript 遇到联合类型时,它不知道当前是哪个具体类型。类型守卫就是告诉 TypeScript:"在这个范围内,变量是某个具体类型"。
// 联合类型:可能是字符串或数字
function process(value: string | number) {
// 这里 value 是 string | number
// value.toUpperCase(); // ❌ 编译错误:number 没有 toUpperCase
}
5.2 内置类型守卫
typeof 守卫
function format(value: string | number): string {
if (typeof value === "string") {
// 这里 TypeScript 知道 value 是 string
return value.toUpperCase();
} else {
// 这里 TypeScript 知道 value 是 number
return value.toFixed(2);
}
}
// 处理前端常见类型
function handleInput(value: string | number | boolean | null | undefined) {
if (typeof value === "string") {
console.log("字符串:", value.trim());
} else if (typeof value === "number") {
console.log("数字:", value.toFixed(2));
} else if (typeof value === "boolean") {
console.log("布尔:", value ? "是" : "否");
} else if (value === null) {
console.log("空值");
} else {
// 这里 value 是 undefined
console.log("未定义");
}
}
instanceof 守卫
class ApiError extends Error {
code: number;
constructor(message: string, code: number) {
super(message);
this.code = code;
}
}
class NetworkError extends Error {
status: number;
constructor(message: string, status: number) {
super(message);
this.status = status;
}
}
function handleError(error: Error | ApiError | NetworkError) {
if (error instanceof ApiError) {
console.log("API 错误:", error.code, error.message);
} else if (error instanceof NetworkError) {
console.log("网络错误:", error.status, error.message);
} else {
console.log("普通错误:", error.message);
}
}
in 操作符守卫
interface User {
id: number;
name: string;
email: string;
}
interface Guest {
sessionId: string;
ip: string;
}
function greet(person: User | Guest) {
if ("email" in person) {
// person 是 User
console.log(`欢迎回来,${person.name} (${person.email})`);
} else {
// person 是 Guest
console.log(`欢迎,访客 ${person.sessionId}`);
}
}
// 前端路由守卫
interface AdminRoute {
path: string;
requiresAdmin: true;
adminOnlyFeatures: string[];
}
interface PublicRoute {
path: string;
requiresAdmin?: false;
}
function canAccess(route: AdminRoute | PublicRoute, isAdmin: boolean) {
if ("requiresAdmin" in route && route.requiresAdmin === true) {
return isAdmin;
}
return true;
}
5.3 自定义类型守卫
当内置守卫不够用时,可以创建自定义类型守卫函数:
// 类型谓词:返回值的类型是 "arg is Type"
function isString(value: unknown): value is string {
return typeof value === "string";
}
function isNumber(value: unknown): value is number {
return typeof value === "number" && !isNaN(value);
}
function isArray<T>(value: unknown): value is T[] {
return Array.isArray(value);
}
// 前端 API 响应守卫
interface SuccessResponse<T> {
success: true;
data: T;
}
interface ErrorResponse {
success: false;
error: string;
code: number;
}
type ApiResponse<T> = SuccessResponse<T> | ErrorResponse;
function isSuccessResponse<T>(
response: ApiResponse<T>
): response is SuccessResponse<T> {
return response.success === true;
}
// 使用
async function fetchUser(id: number) {
const response: ApiResponse<{ id: number; name: string }> =
await fetch(`/api/users/${id}`).then(res => res.json());
if (isSuccessResponse(response)) {
// response 被收窄为 SuccessResponse
console.log("用户数据:", response.data);
return response.data;
} else {
// response 被收窄为 ErrorResponse
console.error("API 错误:", response.error, response.code);
throw new Error(response.error);
}
}
5.4 判别联合(Discriminated Unions)
这是 TypeScript 中最强大的类型收窄模式,通过一个公共的"标签"字段来区分联合类型:
// 前端表单字段类型
type FormField =
| { type: "text"; value: string; placeholder?: string; maxLength?: number }
| { type: "number"; value: number; min?: number; max?: number; step?: number }
| { type: "select"; value: string; options: Array<{ label: string; value: string }> }
| { type: "checkbox"; value: boolean; label: string }
| { type: "date"; value: Date; minDate?: Date; maxDate?: Date };
function renderField(field: FormField) {
switch (field.type) {
case "text":
// field.value 是 string
return `<input type="text" value="${field.value}" maxlength="${field.maxLength}">`;
case "number":
// field.value 是 number
return `<input type="number" value="${field.value}" min="${field.min}">`;
case "select":
// field.value 是 string
const options = field.options.map(opt =>
`<option value="${opt.value}" ${opt.value === field.value ? "selected" : ""}>${opt.label}</option>`
);
return `<select>${options.join("")}</select>`;
case "checkbox":
// field.value 是 boolean
return `<input type="checkbox" ${field.value ? "checked" : ""}> ${field.label}`;
case "date":
// field.value 是 Date
return `<input type="date" value="${field.value.toISOString().split("T")[0]}">`;
}
}
// 前端路由守卫
type Route =
| { path: "/"; component: "Home"; requiresAuth: false }
| { path: "/login"; component: "Login"; requiresAuth: false }
| { path: "/dashboard"; component: "Dashboard"; requiresAuth: true; permissions: string[] }
| { path: "/admin"; component: "Admin"; requiresAuth: true; adminOnly: true };
function canNavigate(route: Route, user: { isAuthenticated: boolean; isAdmin: boolean }) {
if (!route.requiresAuth) return true;
if (!user.isAuthenticated) return false;
if ("adminOnly" in route && route.adminOnly && !user.isAdmin) return false;
return true;
}
5.5 穷尽性检查(Exhaustiveness Checking)
确保 switch 或 if-else 覆盖了所有可能的情况:
type Status = "pending" | "loading" | "success" | "error";
function handleStatus(status: Status) {
switch (status) {
case "pending":
return "等待中";
case "loading":
return "加载中";
case "success":
return "成功";
case "error":
return "错误";
default:
// 如果 Status 类型新增了成员,这里会报编译错误
const _exhaustiveCheck: never = status;
return _exhaustiveCheck;
}
}
// 或者使用函数
function assertNever(x: never): never {
throw new Error(`Unexpected object: ${x}`);
}
function handleStatus2(status: Status) {
switch (status) {
case "pending": return "等待中";
case "loading": return "加载中";
case "success": return "成功";
case "error": return "错误";
default:
return assertNever(status); // 如果 status 不是 never,会报编译错误
}
}
5.6 前端实战:API 响应处理
// 1. 定义 API 响应类型
type ApiResult<T> =
| { status: "idle"; data: null }
| { status: "loading"; data: T | null }
| { status: "success"; data: T }
| { status: "error"; data: null; error: string };
// 2. 创建自定义守卫
function isLoading<T>(result: ApiResult<T>): result is { status: "loading"; data: T | null } {
return result.status === "loading";
}
function isSuccess<T>(result: ApiResult<T>): result is { status: "success"; data: T } {
return result.status === "success";
}
function isError<T>(result: ApiResult<T>): result is { status: "error"; data: null; error: string } {
return result.status === "error";
}
// 3. 使用
function renderApiResult<T>(result: ApiResult<T>, renderData: (data: T) => React.ReactNode) {
if (isLoading(result)) {
return <div>加载中...</div>;
}
if (isSuccess(result)) {
return <div>{renderData(result.data)}</div>;
}
if (isError(result)) {
return <div className="error">错误: {result.error}</div>;
}
// 默认:idle 状态
return <div>准备加载</div>;
}
// 4. React Hook 封装
function useApi<T>(fetchFn: () => Promise<T>) {
const [result, setResult] = useState<ApiResult<T>>({ status: "idle", data: null });
const fetchData = useCallback(async () => {
setResult({ status: "loading", data: result.status === "success" ? result.data : null });
try {
const data = await fetchFn();
setResult({ status: "success", data });
} catch (error) {
setResult({
status: "error",
data: null,
error: error instanceof Error ? error.message : String(error)
});
}
}, [fetchFn]);
return { result, fetchData };
}
// 5. 使用示例
interface User { id: number; name: string; }
const { result, fetchData } = useApi<User>(() =>
fetch("/api/user/1").then(res => res.json())
);
useEffect(() => { fetchData(); }, []);
return (
<div>
{renderApiResult(result, (user) => (
<div>
<h1>{user.name}</h1>
<p>ID: {user.id}</p>
</div>
))}
</div>
);
5.7 前端实战:表单验证
// 表单字段验证结果
type ValidationResult =
| { valid: true; value: string }
| { valid: false; value: string; error: string };
// 验证函数
function validateEmail(email: string): ValidationResult {
if (!email.includes("@")) {
return { valid: false, value: email, error: "邮箱格式不正确" };
}
return { valid: true, value: email };
}
function validatePassword(password: string): ValidationResult {
if (password.length < 6) {
return { valid: false, value: password, error: "密码至少6位" };
}
return { valid: true, value: password };
}
// 使用验证结果
function handleSubmit(email: string, password: string) {
const emailResult = validateEmail(email);
const passwordResult = validatePassword(password);
if (!emailResult.valid) {
showError(emailResult.error);
return;
}
if (!passwordResult.valid) {
showError(passwordResult.error);
return;
}
// 这里 TypeScript 知道 emailResult 和 passwordResult 都是 valid: true
// 所以可以安全地访问 .value
submitForm(emailResult.value, passwordResult.value);
}
5.8 类型收窄的实用技巧
可选链与空值合并
interface User {
profile?: {
avatar?: {
url?: string;
};
};
}
// 传统写法
function getAvatarUrl(user: User): string | undefined {
if (user.profile && user.profile.avatar && user.profile.avatar.url) {
return user.profile.avatar.url;
}
return undefined;
}
// 可选链写法
function getAvatarUrl2(user: User): string | undefined {
return user.profile?.avatar?.url;
}
// 空值合并
function getAvatarUrl3(user: User): string {
return user.profile?.avatar?.url ?? "/default-avatar.png";
}
非空断言(谨慎使用)
// 当你知道某个值一定存在时
const element = document.getElementById("app")!; // 非空断言
element.classList.add("loaded");
// 更好的做法:用类型守卫
const element = document.getElementById("app");
if (element) {
element.classList.add("loaded");
}
类型断言函数
function assert(condition: any, msg?: string): asserts condition {
if (!condition) {
throw new Error(msg || "Assertion failed");
}
}
function assertIsString(value: unknown): asserts value is string {
if (typeof value !== "string") {
throw new Error("Value is not a string");
}
}
// 使用
function process(value: unknown) {
assertIsString(value);
// 这里 TypeScript 知道 value 是 string
console.log(value.toUpperCase());
}
总结:类型守卫是处理前端"不确定性数据"的核心工具。通过
typeof、instanceof、in、自定义守卫和判别联合,你可以让 TypeScript 理解代码的运行时逻辑,提供更精准的类型检查和智能提示。判别联合模式特别适合前端状态管理、路由守卫、表单验证等场景。
6.1 工具类型概述
TypeScript 内置了大量工具类型,它们可以基于已有类型快速生成新类型,避免重复定义。对前端开发而言,工具类型是真正的"生产力倍增器"。
核心工具类型一览:
| 工具类型 | 作用 | 前端典型场景 |
|---|---|---|
Partial<T> | 所有属性变为可选 | 表单编辑模式草稿、组件默认值合并 |
Required<T> | 所有属性变为必填 | 表单提交时的完整校验 |
Readonly<T> | 所有属性变为只读 | 不可变状态、常量配置 |
Pick<T, K> | 从 T 中选取指定属性 | 组件只取需要的 Props 子集 |
Omit<T, K> | 从 T 中排除指定属性 | 继承父组件 Props 但排除某些属性 |
Record<K, V> | 构造键为 K、值为 V 的对象 | 路由映射、字典、配置表 |
Exclude<T, U> | 从联合类型 T 中排除 U | 过滤状态类型、权限过滤 |
Extract<T, U> | 从联合类型 T 中提取 U | 提取特定状态、类型筛选 |
NonNullable<T> | 排除 null 和 undefined | 确保值非空 |
ReturnType<T> | 获取函数返回值类型 | 提取第三方库函数返回类型 |
Parameters<T> | 获取函数参数元组类型 | 提取回调函数参数类型 |
Awaited<T> | 解包 Promise 类型 | 提取 API 函数实际返回数据 |
6.2 Partial、Required、Readonly
表单场景:编辑模式 vs 提交模式
// 完整表单数据结构
interface UserForm {
username: string;
email: string;
password: string;
bio: string;
avatar: string;
}
// 编辑模式:用户可能只改部分字段(Partial)
function updateUser(id: number, data: Partial<UserForm>) {
// data 中所有字段都是可选的
fetch(`/api/users/${id}`, {
method: "PATCH",
body: JSON.stringify(data),
});
}
// 使用
updateUser(1, { bio: "新的简介" }); // ✅ 只更新 bio
updateUser(1, { email: "new@email.com", bio: "new bio" }); // ✅ 更新多个字段
// 如果某个中间环节需要所有字段
function validateUser(data: Required<UserForm>): boolean {
// data 中所有字段都不能是 undefined
return true;
}
只读配置
// 不可变的配置对象
interface AppConfig {
apiBaseUrl: string;
timeout: number;
retryCount: number;
features: string[];
}
// 冻结的配置
const defaultConfig: Readonly<AppConfig> = {
apiBaseUrl: "https://api.example.com",
timeout: 5000,
retryCount: 3,
features: ["auth", "payment"],
};
// defaultConfig.apiBaseUrl = "..."; // ❌ 编译错误
// defaultConfig.features.push("..."); // ❌ Readonly 作用于顶层,数组本身也可能受限(需配合 as const)
6.3 Pick 和 Omit:属性选取与排除
这两个是前端组件 Props 设计中最常用的工具类型:
// 基础组件 Props
interface ButtonProps {
variant: "primary" | "secondary" | "danger";
size: "small" | "medium" | "large";
disabled: boolean;
loading: boolean;
icon?: React.ReactNode;
onClick: (event: React.MouseEvent) => void;
className?: string;
style?: React.CSSProperties;
ariaLabel?: string;
}
// 图标按钮:只需要 variant、size、icon、onClick、ariaLabel
type IconButtonProps = Pick<ButtonProps, "variant" | "size" | "icon" | "onClick" | "ariaLabel">;
// 文本按钮:除了 icon 之外的所有属性
type TextButtonProps = Omit<ButtonProps, "icon">;
// 简化版按钮(给非技术用户使用):省略复杂属性
type SimpleButtonProps = Omit<ButtonProps, "loading" | "ariaLabel" | "style">;
前端实战:表单类型变体
interface Product {
id: string;
name: string;
price: number;
description: string;
category: string;
stock: number;
createdAt: string;
updatedAt: string;
}
// 创建产品:不需要 id、createdAt、updatedAt
type CreateProductDTO = Omit<Product, "id" | "createdAt" | "updatedAt">;
// 编辑产品:所有业务字段都可选,但必须有 id 来定位
type UpdateProductDTO = Pick<Product, "id"> & Partial<Omit<Product, "id" | "createdAt" | "updatedAt">>;
// 列表展示:只需要名称、价格、分类、库存
type ProductListItem = Pick<Product, "id" | "name" | "price" | "category" | "stock">;
// 详情展示:所有字段(但创建/更新时间用格式化后的字符串)
type ProductDetail = Omit<Product, "createdAt" | "updatedAt"> & {
createdAtFormatted: string;
updatedAtFormatted: string;
};
6.4 Record:字典与映射
Record<K, V> 用于创建"键值对映射"类型,在前端配置、路由、字典场景中不可或缺:
// 路由路径 → 页面标题的映射
type RouteTitles = Record<string, string>;
const routeTitles: RouteTitles = {
"/": "首页",
"/dashboard": "控制台",
"/settings": "设置",
"/profile": "个人中心",
};
// 权限 → 菜单列表的映射
type Permission = "admin" | "editor" | "viewer";
type MenuConfig = Record<Permission, Array<{ path: string; label: string }>>;
const menuConfig: MenuConfig = {
admin: [
{ path: "/admin/users", label: "用户管理" },
{ path: "/admin/settings", label: "系统设置" },
],
editor: [
{ path: "/editor/posts", label: "文章管理" },
],
viewer: [
{ path: "/posts", label: "浏览文章" },
],
};
// HTTP 状态码 → 错误消息映射
type ErrorMessages = Record<number, string>;
const errorMessages: ErrorMessages = {
400: "请求参数错误",
401: "未登录或登录已过期",
403: "没有访问权限",
404: "资源不存在",
500: "服务器内部错误",
};
// 前端国际化:语言 → 翻译键 → 翻译文本
type Locale = "zh-CN" | "en-US" | "ja-JP";
type TranslationMap = Record<Locale, Record<string, string>>;
// 将枚举值映射为选项数组的工具函数
function enumToOptions<T extends string>(enumObj: Record<string, T>): Array<{ label: string; value: T }> {
return Object.values(enumObj).map(value => ({
label: value,
value,
}));
}
enum Theme {
Light = "light",
Dark = "dark",
System = "system",
}
const themes = enumToOptions(Theme);
// [{ label: "light", value: "light" }, { label: "dark", value: "dark" }, ...]
6.5 ReturnType 和 Parameters
这两个工具类型能极大简化与第三方库的交互:
// 提取函数返回值类型
function fetchUser(id: number) {
return fetch(`/api/users/${id}`).then(res => res.json() as Promise<{ id: number; name: string }>);
}
type FetchUserReturnType = ReturnType<typeof fetchUser>;
// Promise<{ id: number; name: string }>
// 使用时
type User = Awaited<ReturnType<typeof fetchUser>>;
// { id: number; name: string }
// 提取回调函数参数类型
function Button({ onClick }: { onClick: (event: React.MouseEvent, id: string) => void }) {
return <button>点击</button>;
}
type ButtonClickHandler = Parameters<typeof Button>["0"]["onClick"];
// (event: React.MouseEvent, id: string) => void
// 从第三方库提取类型
import { useSelector } from "react-redux";
type UseSelectorType = ReturnType<typeof useSelector>;
// 不需要去翻文档找 useSelect 的返回类型
// 从组件 Props 提取事件类型
type InputProps = {
onChange: (value: string) => void;
onFocus: () => void;
onBlur: () => void;
};
type OnChangeHandler = InputProps["onChange"]; // (value: string) => void
type OnChangeParam = Parameters<InputProps["onChange"]>[0]; // string
6.6 Awaited:解包 Promise
// 异步函数
async function getUser(): Promise<{ id: number; name: string; email: string }> {
const res = await fetch("/api/user");
return res.json();
}
// Awaited 提取 Promise 的内部类型
type User = Awaited<ReturnType<typeof getUser>>;
// { id: number; name: string; email: string }
// 嵌套 Promise 也能解包
type Nested = Awaited<Promise<Promise<Promise<string>>>>;
// string
// 配合 API 层使用
async function getProductList(): Promise<{
items: Array<{ id: number; name: string; price: number }>;
total: number;
}> {
return fetch("/api/products").then(res => res.json());
}
type ProductListData = Awaited<ReturnType<typeof getProductList>>;
// 自动获得完整类型,无需手动维护 interface
6.7 Exclude 和 Extract
处理联合类型时的筛选和提取:
// 排除
type Status = "idle" | "loading" | "success" | "error";
type TransientStatus = Exclude<Status, "success" | "error">;
// "idle" | "loading"
// 提取
type FinalStatus = Extract<Status, "success" | "error">;
// "success" | "error"
// 前端事件类型提取
type EventTypes =
| React.MouseEvent
| React.KeyboardEvent
| React.ChangeEvent<HTMLInputElement>
| React.FormEvent<HTMLFormElement>;
// 提取键盘相关事件
type KeyboardRelated = Extract<EventTypes, React.KeyboardEvent<HTMLElement>>;
// React.KeyboardEvent
// NonNullable
type MaybeString = string | null | undefined;
type DefinitelyString = NonNullable<MaybeString>;
// string
6.8 前端实战组合案例
案例:通用表格组件
// 表格列配置
interface ColumnConfig<T> {
key: keyof T;
title: string;
render?: (value: T[keyof T], record: T) => React.ReactNode;
sortable?: boolean;
filterable?: boolean;
}
// 表格 Props
interface TableProps<T> {
data: T[];
columns: ColumnConfig<T>[];
loading?: boolean;
emptyText?: string;
rowKey?: keyof T;
// 排序
sortBy?: keyof T;
sortOrder?: "asc" | "desc";
onSort?: (key: keyof T) => void;
// 行点击
onRowClick?: (record: T) => void;
// 分页
pagination?: {
current: number;
pageSize: number;
total: number;
onChange: (page: number) => void;
};
}
// 实际使用
interface UserRecord {
id: number;
name: string;
email: string;
role: "admin" | "user";
createdAt: string;
}
// 列配置——类型安全!
const userColumns: ColumnConfig<UserRecord>[] = [
{ key: "id", title: "ID" },
{
key: "name",
title: "姓名",
render: (value, record) => (
<a href={`/users/${record.id}`}>{value}</a>
)
},
{ key: "email", title: "邮箱" },
{
key: "role",
title: "角色",
render: (value) => value === "admin" ? "管理员" : "普通用户"
},
{ key: "createdAt", title: "注册时间" },
];
// 表格使用
function UserTable(props: Omit<TableProps<UserRecord>, "columns">) {
return <GenericTable<UserRecord> columns={userColumns} {...props} />;
}
案例:Redux Slice 辅助类型
import { createSlice, PayloadAction } from "@reduxjs/toolkit";
// 从 Slice Actions 中提取类型
const counterSlice = createSlice({
name: "counter",
initialState: { value: 0 },
reducers: {
increment: (state) => { state.value += 1; },
decrement: (state) => { state.value -= 1; },
incrementByAmount: (state, action: PayloadAction<number>) => {
state.value += action.payload;
},
},
});
// 提取 Action Creator 类型
type CounterActions = typeof counterSlice.actions;
// 提取特定 Action 类型
type IncrementByAmountAction = ReturnType<CounterActions["incrementByAmount"]>;
// { type: "counter/incrementByAmount"; payload: number }
// 提取 State 类型
type CounterState = ReturnType<typeof counterSlice.getInitialState>;
// { value: number }
案例:API 请求的类型安全封装
// API 函数定义映射
interface ApiFunctions {
getUser: (id: number) => Promise<{ id: number; name: string; email: string }>;
getProducts: (page: number) => Promise<{ items: Array<{ id: number; name: string }>; total: number }>;
createUser: (data: { name: string; email: string }) => Promise<{ id: number }>;
}
// 通用 API 调用 Hook
function useApiCall<K extends keyof ApiFunctions>(
apiName: K,
...args: Parameters<ApiFunctions[K]>
): {
data: Awaited<ReturnType<ApiFunctions[K]>> | null;
loading: boolean;
} {
// 实现省略
return { data: null, loading: false };
}
// 使用时自动推导类型
const { data: user } = useApiCall("getUser", 1);
// user: { id: number; name: string; email: string } | null
const { data: products } = useApiCall("getProducts", 1);
// products: { items: Array<{ id: number; name: string }>; total: number } | null
总结:工具类型的核心价值在于「类型复用而非类型拷贝」。在前端项目中,
Pick/Omit解决组件 Props 继承问题,Partial/Required解决表单场景的类型转换,Record解决配置映射问题,ReturnType/Parameters/Awaited解决与第三方代码的类型联动。掌握这些工具类型后,你可以用更少的代码定义更精确的类型。
7.1 React 中的 TypeScript
7.1.1 函数组件与 Props 类型
// 基础 Props
interface UserCardProps {
name: string;
email: string;
avatar?: string;
isOnline?: boolean;
}
// 方式一:直接注解
function UserCard({ name, email, avatar, isOnline = false }: UserCardProps) {
return (
<div className="user-card">
{avatar && <img src={avatar} alt={name} />}
<h3>{name}</h3>
<p>{email}</p>
<span>{isOnline ? "在线" : "离线"}</span>
</div>
);
}
// 方式二:React.FC(不再推荐,但常见于旧代码)
const UserCard: React.FC<UserCardProps> = ({ name, email }) => {
return <div>{name}</div>;
};
// 不推荐原因:隐式包含 children,且不支持泛型组件
// 带 children 的组件
interface CardProps {
title: string;
children: React.ReactNode; // 推荐
// children?: React.ReactNode; // 可选 children
// header?: React.ReactNode; // 其他命名插槽
// footer?: React.ReactNode;
}
function Card({ title, children }: CardProps) {
return (
<div className="card">
<h2>{title}</h2>
<div className="card-body">{children}</div>
</div>
);
}
7.1.2 常用 Hook 类型
// useState
const [count, setCount] = useState<number>(0); // 自动推断
const [user, setUser] = useState<User | null>(null); // 初始 null 需显式类型
const [users, setUsers] = useState<User[]>([]); // 空数组仍需注解
// useRef
// 1. 只读 DOM 引用
const inputRef = useRef<HTMLInputElement>(null);
// inputRef.current?.focus();
// 2. 可变值(与渲染无关)
const timerRef = useRef<number | null>(null);
timerRef.current = window.setTimeout(() => {}, 1000);
// useReducer
type State = { count: number; step: number };
type Action =
| { type: "increment" }
| { type: "decrement" }
| { type: "reset"; payload: number };
function reducer(state: State, action: Action): State {
switch (action.type) {
case "increment": return { ...state, count: state.count + state.step };
case "decrement": return { ...state, count: state.count - state.step };
case "reset": return { ...state, count: action.payload };
}
}
const [state, dispatch] = useReducer(reducer, { count: 0, step: 1 });
// useContext
interface ThemeContextType {
theme: "light" | "dark";
toggleTheme: () => void;
}
const ThemeContext = createContext<ThemeContextType | null>(null);
function useTheme() {
const context = useContext(ThemeContext);
if (!context) {
throw new Error("useTheme must be used within ThemeProvider");
}
return context;
}
// useCallback
const handleClick = useCallback((id: number) => {
console.log("Clicked:", id);
}, []);
// useMemo
const sortedUsers = useMemo(() => {
return [...users].sort((a, b) => a.name.localeCompare(b.name));
}, [users]);
7.1.3 事件处理类型
// 常用事件类型速查
function EventHandlersDemo() {
// 点击事件
const handleClick = (event: React.MouseEvent<HTMLButtonElement>) => {
console.log(event.clientX, event.clientY);
};
// 输入变化
const handleChange = (event: React.ChangeEvent<HTMLInputElement>) => {
console.log(event.target.value); // string
};
// 表单提交
const handleSubmit = (event: React.FormEvent<HTMLFormElement>) => {
event.preventDefault();
};
// 键盘事件
const handleKeyDown = (event: React.KeyboardEvent<HTMLInputElement>) => {
if (event.key === "Enter") {
console.log("回车");
}
};
// 焦点事件
const handleFocus = (event: React.FocusEvent<HTMLInputElement>) => {
console.log("focused");
};
// 拖拽事件
const handleDrag = (event: React.DragEvent<HTMLDivElement>) => {
console.log(event.dataTransfer);
};
return (
<form onSubmit={handleSubmit}>
<input
onChange={handleChange}
onKeyDown={handleKeyDown}
onFocus={handleFocus}
/>
<button onClick={handleClick}>提交</button>
</form>
);
}
7.1.4 forwardRef 和 useImperativeHandle
// forwardRef + useImperativeHandle
interface InputHandles {
focus: () => void;
clear: () => void;
}
interface InputProps {
placeholder?: string;
defaultValue?: string;
}
const Input = forwardRef<InputHandles, InputProps>((props, ref) => {
const inputRef = useRef<HTMLInputElement>(null);
useImperativeHandle(ref, () => ({
focus: () => inputRef.current?.focus(),
clear: () => {
if (inputRef.current) {
inputRef.current.value = "";
}
},
}));
return <input ref={inputRef} placeholder={props.placeholder} />;
});
// 使用
function Form() {
const inputRef = useRef<InputHandles>(null);
return (
<div>
<Input ref={inputRef} placeholder="请输入" />
<button onClick={() => inputRef.current?.focus()}>聚焦</button>
<button onClick={() => inputRef.current?.clear()}>清空</button>
</div>
);
}
7.1.5 泛型组件
// React 泛型组件(通过函数签名实现)
interface SelectProps<T> {
value: T;
options: Array<{ label: string; value: T }>;
onChange: (value: T) => void;
}
function Select<T extends string | number>({
value,
options,
onChange,
}: SelectProps<T>) {
return (
<select value={String(value)} onChange={(e) => onChange(e.target.value as T)}>
{options.map((opt) => (
<option key={String(opt.value)} value={String(opt.value)}>
{opt.label}
</option>
))}
</select>
);
}
// 使用
const roleSelect = (
<Select<"admin" | "user" | "guest">
value="user"
options={[
{ label: "管理员", value: "admin" },
{ label: "用户", value: "user" },
{ label: "访客", value: "guest" },
]}
onChange={(value) => {
// value 的类型是 "admin" | "user" | "guest"
console.log(value);
}}
/>
);
7.1.6 React 项目中的类型组织
src/
├── types/
│ ├── api.ts # API 响应/请求类型
│ ├── common.ts # 通用工具类型
│ ├── components.ts # 组件共享 Props 类型
│ └── store.ts # Redux/Zustand Store 类型
├── components/
│ ├── Button/
│ │ ├── Button.tsx
│ │ ├── Button.types.ts # 组件独有类型
│ │ └── index.ts
│ └── Table/
│ ├── Table.tsx
│ ├── Table.types.ts
│ └── index.ts
7.2 Vue 3 中的 TypeScript
7.2.1 Composition API:defineProps / defineEmits
<script setup lang="ts">
// Props 类型定义
interface Props {
title: string;
count?: number;
items: Array<{ id: number; name: string }>;
variant?: "primary" | "secondary" | "danger";
}
const props = withDefaults(defineProps<Props>(), {
count: 0,
variant: "primary",
});
// Emits 类型定义
interface Emits {
(e: "update", value: number): void;
(e: "delete", id: number): void;
(e: "submit"): void;
}
const emit = defineEmits<Emits>();
// 使用
const handleClick = () => {
emit("update", 1); // ✅ 类型安全
// emit("update", "1"); // ❌ 类型错误
emit("delete", 42);
};
</script>
<template>
<div>
<h1>{{ props.title }}</h1>
<p>Count: {{ props.count }}</p>
</div>
</template>
7.2.2 ref / reactive 类型推导
import { ref, reactive, computed, watch } from "vue";
// ref 自动推导
const count = ref(0); // Ref<number>
const message = ref(""); // Ref<string>
const isVisible = ref(true); // Ref<boolean>
// ref 需要注解(初始 null)
interface User { id: number; name: string; }
const user = ref<User | null>(null); // Ref<User | null>
// reactive 类型
interface FormState {
email: string;
password: string;
rememberMe: boolean;
}
const form = reactive<FormState>({
email: "",
password: "",
rememberMe: false,
});
// computed 自动推导
const doubleCount = computed(() => count.value * 2);
// ComputedRef<number>
const fullName = computed<string>(() => {
return `${firstName.value} ${lastName.value}`;
});
// watch 类型
watch(
() => form.email,
(newEmail, oldEmail) => {
console.log("email changed from", oldEmail, "to", newEmail);
}
);
// watchEffect
watchEffect(() => {
if (form.email) {
console.log("validating:", form.email);
}
});
7.2.3 Provide / Inject 类型
// 父组件
import { provide, type InjectionKey } from "vue";
interface ThemeContext {
theme: "light" | "dark";
fontSize: number;
}
// 方式一:InjectionKey(推荐,类型最安全)
const themeKey: InjectionKey<ThemeContext> = Symbol("theme");
function setup() {
provide(themeKey, {
theme: "dark",
fontSize: 14,
});
}
// 子组件
import { inject } from "vue";
function useTheme(): ThemeContext {
const theme = inject(themeKey);
if (!theme) {
throw new Error("themeKey was not provided");
}
return theme;
}
// 使用时完整类型推导
const { theme, fontSize } = useTheme();
7.2.4 模板 Ref
import { ref, onMounted } from "vue";
// 模板 ref
const inputRef = ref<HTMLInputElement | null>(null);
const componentRef = ref<InstanceType<typeof MyComponent> | null>(null);
onMounted(() => {
inputRef.value?.focus();
componentRef.value?.doSomething();
});
7.2.5 defineExpose
<script setup lang="ts">
import { ref } from "vue";
const count = ref(0);
function increment() {
count.value++;
}
function reset() {
count.value = 0;
}
// 暴露给父组件的方法和属性
defineExpose({
count,
increment,
reset,
});
</script>
<!-- 父组件 -->
<script setup lang="ts">
import { ref, onMounted } from "vue";
import Counter from "./Counter.vue";
const counterRef = ref<InstanceType<typeof Counter> | null>(null);
onMounted(() => {
counterRef.value?.increment(); // ✅ 类型安全
console.log(counterRef.value?.count);
});
</script>
<template>
<Counter ref="counterRef" />
</template>
7.2.6 Vue 3 泛型组件
<script setup lang="ts" generic="T extends string | number">
interface Props {
options: Array<{ label: string; value: T }>;
modelValue: T;
}
const props = defineProps<Props>();
const emit = defineEmits<{
(e: "update:modelValue", value: T): void;
}>();
function handleSelect(value: T) {
emit("update:modelValue", value);
}
</script>
<template>
<select
:value="props.modelValue"
@change="handleSelect(($event.target as HTMLSelectElement).value as T)"
>
<option
v-for="opt in props.options"
:key="String(opt.value)"
:value="opt.value"
>
{{ opt.label }}
</option>
</select>
</template>
7.3 框架通用的最佳实践
7.3.1 API 请求层封装
// request.ts — React/Vue 通用
interface RequestConfig {
baseURL?: string;
timeout?: number;
headers?: Record<string, string>;
}
interface RequestInterceptor<T = any> {
onRequest?: (config: RequestConfig) => RequestConfig;
onResponse?: (data: T) => T;
onError?: (error: Error) => void;
}
class HttpClient {
private config: RequestConfig;
constructor(config: RequestConfig = {}) {
this.config = {
timeout: 10000,
...config,
};
}
async get<T>(url: string, params?: Record<string, string>): Promise<T> {
const queryString = params
? "?" + new URLSearchParams(params).toString()
: "";
const response = await fetch(this.config.baseURL + url + queryString);
if (!response.ok) throw new Error(`HTTP ${response.status}`);
return response.json() as Promise<T>;
}
async post<T>(url: string, body: Record<string, any>): Promise<T> {
const response = await fetch(this.config.baseURL + url, {
method: "POST",
headers: { "Content-Type": "application/json", ...this.config.headers },
body: JSON.stringify(body),
});
if (!response.ok) throw new Error(`HTTP ${response.status}`);
return response.json() as Promise<T>;
}
}
export const http = new HttpClient({ baseURL: "https://api.example.com" });
// 使用(React/Vue 通用)
interface LoginResponse { token: string; user: { id: number; name: string; }; }
const result = await http.post<LoginResponse>("/auth/login", {
email: "user@example.com",
password: "123456",
});
// result: LoginResponse — 类型已推导
7.3.2 状态管理通用模式
// 通用 Store 类型(React Zustand / Vue Pinia 通用思路)
interface StoreState<T> {
data: T | null;
loading: boolean;
error: string | null;
}
interface StoreActions<T> {
setData: (data: T) => void;
setLoading: (loading: boolean) => void;
setError: (error: string | null) => void;
reset: () => void;
}
// 工厂函数
function createStoreActions<T>(
set: (fn: (state: StoreState<T>) => Partial<StoreState<T>>) => void,
get: () => StoreState<T>
): StoreActions<T> {
return {
setData: (data) => set(() => ({ data, loading: false, error: null })),
setLoading: (loading) => set(() => ({ loading })),
setError: (error) => set(() => ({ error, loading: false })),
reset: () => set(() => ({ data: null, loading: false, error: null })),
};
}
7.3.3 路由类型安全
// 路由配置
interface Route<Path extends string> {
path: Path;
component: React.ComponentType<any>;
meta?: {
title: string;
requiresAuth?: boolean;
permissions?: string[];
};
}
// 路由参数
interface RouteParams {
"/user/:id": { id: string };
"/post/:postId/comment/:commentId": { postId: string; commentId: string };
"/settings": {};
"/": {};
}
// 类型安全的路由参数 Hook(简化版)
function useParams<P extends keyof RouteParams>(path: P): RouteParams[P] {
// 实际实现取决于路由库
return {} as RouteParams[P];
}
const params = useParams("/user/:id");
// params.id: string ✅ 类型安全
总结:在 React 和 Vue 中使用 TypeScript 的核心原则是**「类型推导优于显式注解」**。React 的 useState、useRef、useContext,Vue 3 的 ref、reactive、computed 都有出色的类型推导能力。你只需在"类型边界"处显式注解——组件 Props、API 返回值、事件处理函数参数——其余地方让 TypeScript 自动推导。
8.1 .d.ts 声明文件的作用
.d.ts 文件是 TypeScript 的类型声明文件,只包含类型信息,不包含实现。它们用于:
- 为无类型的 JavaScript 库提供类型支持
- 声明全局变量和模块
- 扩展第三方库的类型
// my-lib.d.ts
declare module "my-untyped-lib" {
export function greet(name: string): string;
export const version: string;
}
// 使用
import { greet } from "my-untyped-lib"; // 现在有类型了
8.2 declare 关键字
declare 告诉 TypeScript:"这个变量/函数/类在别处已经存在,你只需要知道它的类型"。
// 全局变量声明
declare const API_BASE_URL: string;
declare const IS_DEVELOPMENT: boolean;
// 全局函数
declare function log(message: string, level?: "info" | "warn" | "error"): void;
// 全局类
declare class Analytics {
track(event: string, data: Record<string, any>): void;
pageView(path: string): void;
}
// 命名空间
declare namespace MyApp {
interface Config {
apiUrl: string;
timeout: number;
}
function init(config: Config): void;
}
8.3 全局类型扩展
扩展 Window 对象
// global.d.ts
interface Window {
myCustomProperty: string;
myCustomMethod: () => void;
analytics: {
track: (event: string) => void;
};
}
// 使用
window.myCustomProperty = "value";
window.analytics.track("page_view");
扩展 Vue 全局属性
// vue.d.ts
import { ComponentCustomProperties } from "vue";
declare module "vue" {
interface ComponentCustomProperties {
$filters: {
formatDate: (date: Date | string) => string;
formatCurrency: (amount: number) => string;
};
$api: {
getUser: (id: number) => Promise<{ id: number; name: string }>;
};
}
}
扩展 React 全局类型
// react.d.ts
import "react";
declare module "react" {
interface CSSProperties {
// 添加自定义 CSS 属性
"--theme-color"?: string;
"--spacing"?: string;
}
}
8.4 declare module:为无类型模块添加类型
// 为 .vue 文件声明类型(Vue 3)
declare module "*.vue" {
import { DefineComponent } from "vue";
const component: DefineComponent<{}, {}, any>;
export default component;
}
// 为图片文件声明类型
declare module "*.png" {
const src: string;
export default src;
}
declare module "*.jpg" {
const src: string;
export default src;
}
declare module "*.svg" {
const src: string;
export default src;
}
// 为 CSS/SCSS 文件声明类型
declare module "*.css" {
const classes: { readonly [key: string]: string };
export default classes;
}
declare module "*.scss" {
const classes: { readonly [key: string]: string };
export default classes;
}
// 为 JSON 文件声明类型
declare module "*.json" {
const value: any;
export default value;
}
8.5 @types 组织
当使用 npm install @types/react 时,你安装的是 DefinitelyTyped 仓库中的类型声明包。这些包会自动被 TypeScript 识别。
# 安装常用库的类型声明
npm install --save-dev @types/react @types/react-dom @types/node
npm install --save-dev @types/lodash @types/axios @types/jest
npm install --save-dev @types/express @types/webpack @types/express-session
8.6 快速声明方案
方案一:快速 .d.ts 文件
// types/untyped-lib.d.ts
declare module "untyped-lib" {
// 快速声明核心 API
export function doSomething(input: string): number;
export const version: string;
// 其他 API 用 any 快速声明
export const config: any;
export function advancedFeature(...args: any[]): any;
}
方案二:类型补丁
// types/patch.d.ts
import "some-library";
declare module "some-library" {
// 补充缺失的类型
export interface SomeInterface {
missingField?: string;
}
// 修正错误的类型
export function someFunction(): string; // 原声明返回 any
}
方案三:模块重导出
// types/wrapper.ts
import * as untypedLib from "untyped-lib";
// 包装并添加类型
export interface TypedResult {
data: any;
status: number;
}
export function typedFetch(url: string): Promise<TypedResult> {
return untypedLib.fetch(url) as Promise<TypedResult>;
}
// 使用包装后的版本
import { typedFetch } from "./types/wrapper";
8.7 三斜线指令
三斜线指令用于声明文件间的依赖关系:
/// <reference types="react" />
/// <reference path="./custom.d.ts" />
/// <reference lib="es2015" />
// 使用场景
// 1. 依赖其他声明文件
// 2. 指定编译目标库
// 3. 声明模块依赖
8.8 前端实战:为内部 NPM 包写声明
// packages/shared-utils/types/index.d.ts
export interface User {
id: number;
name: string;
email: string;
}
export interface Paginated<T> {
items: T[];
total: number;
page: number;
pageSize: number;
}
export function formatDate(date: Date | string): string;
export function formatCurrency(amount: number, currency?: string): string;
export function debounce<T extends (...args: any[]) => any>(
fn: T,
delay: number
): (...args: Parameters<T>) => void;
export const constants: {
API_BASE_URL: string;
MAX_FILE_SIZE: number;
SUPPORTED_LOCALES: string[];
};
// 在 package.json 中指定类型文件
{
"name": "shared-utils",
"version": "1.0.0",
"main": "dist/index.js",
"types": "types/index.d.ts", // 关键字段
"exports": {
".": {
"import": "./dist/index.mjs",
"require": "./dist/index.js",
"types": "./types/index.d.ts"
}
}
}
总结:类型声明文件是 TypeScript 生态的基石。掌握
declare、declare module、全局类型扩展三大核心能力,就可以应对 90% 的第三方库集成场景。对于无类型库,优先从 DefinitelyTyped 找 @types 包,其次手写最小声明,最后才考虑模块包装。
9.1 tsconfig.json 核心配置详解
tsconfig.json 是 TypeScript 项目的配置文件,放在项目根目录,告诉 TypeScript 如何编译你的代码。
完整配置示例
{
"compilerOptions": {
"target": "ES2020",
"lib": ["ES2020", "DOM", "DOM.Iterable"],
"module": "ESNext",
"moduleResolution": "bundler",
"baseUrl": ".",
"paths": {
"@/*": ["src/*"],
"@components/*": ["src/components/*"]
},
"outDir": "./dist",
"declaration": true,
"declarationMap": true,
"sourceMap": true,
"strict": true,
"noImplicitAny": true,
"strictNullChecks": true,
"strictFunctionTypes": true,
"strictBindCallApply": true,
"strictPropertyInitialization": true,
"noImplicitThis": true,
"alwaysStrict": true,
"noUnusedLocals": true,
"noUnusedParameters": true,
"noImplicitReturns": true,
"noFallthroughCasesInSwitch": true,
"esModuleInterop": true,
"allowSyntheticDefaultImports": true,
"forceConsistentCasingInFileNames": true,
"jsx": "react-jsx",
"skipLibCheck": true,
"resolveJsonModule": true,
"isolatedModules": true
},
"include": ["src/**/*"],
"exclude": ["node_modules", "dist"]
}
9.2 核心配置项逐条解析
target 和 lib
- target:编译到哪个 JS 版本。
ES2020是前端最佳平衡点——可选链?.、空值合并??、动态import()都能得到原生支持。 - lib:告诉 TypeScript"哪些内置 API 是可用的"。必须包含
"DOM"才能用document、window、console等浏览器 API。
// 如果 lib 没包含 "DOM"
const el = document.getElementById("app"); // Error: 找不到 'document'
moduleResolution
| 策略 | 适用场景 |
|---|---|
"node" | 传统 Node.js CommonJS 解析 |
"node16" / "nodenext" | 支持 ES module 和 package.json exports |
"bundler" | 前端项目首选,模拟 Vite/Webpack 的解析行为 |
"bundler" 模式支持打包器风格的导入:
import Button from "@/components/Button"; // 路径别名,无需扩展名
import "./styles"; // 无扩展名导入
注意:
paths只影响 TS 的类型检查,还需要在 Vite/Webpack 中配置对应别名。
strict 系列详解
// noImplicitAny:禁止隐式 any
function greet(name) { // Error: 参数 'name' 隐式具有 'any' 类型
return `Hello, ${name}`;
}
// strictNullChecks:严格区分 null/undefined
const user = users.find(u => u.id === 1);
// user.name; // Error: 'user' 可能为 'undefined'
// strictPropertyInitialization:类属性必须初始化
class User {
name: string; // Error: 属性未初始化
}
// noImplicitReturns:函数必须有返回值
function getStatus(code: number): string {
if (code === 200) return "OK";
if (code === 404) return "Not Found";
// Error: 并非所有代码路径都返回值
}
// noFallthroughCasesInSwitch
switch (status) {
case "success":
console.log("OK");
// Error: 未包含 break
case "error":
console.log("Error");
break;
}
9.3 React 项目配置
{
"compilerOptions": {
"target": "ES2020",
"lib": ["ES2020", "DOM", "DOM.Iterable"],
"module": "ESNext",
"moduleResolution": "bundler",
"baseUrl": ".",
"paths": { "@/*": ["src/*"] },
"jsx": "react-jsx",
"strict": true,
"esModuleInterop": true,
"skipLibCheck": true,
"forceConsistentCasingInFileNames": true,
"resolveJsonModule": true,
"isolatedModules": true
},
"include": ["src/**/*"],
"exclude": ["node_modules", "build", "dist"]
}
9.4 Vue 3 项目配置
{
"compilerOptions": {
"target": "ES2020",
"lib": ["ES2020", "DOM", "DOM.Iterable"],
"module": "ESNext",
"moduleResolution": "bundler",
"baseUrl": ".",
"paths": { "@/*": ["src/*"] },
"jsx": "preserve",
"strict": true,
"esModuleInterop": true,
"skipLibCheck": true,
"forceConsistentCasingInFileNames": true,
"resolveJsonModule": true,
"isolatedModules": true,
"types": ["vite/client"]
},
"include": [
"src/**/*.ts",
"src/**/*.d.ts",
"src/**/*.tsx",
"src/**/*.vue"
],
"exclude": ["node_modules", "dist"]
}
9.5 Monorepo 项目引用配置
// 根目录 tsconfig.json
{
"compilerOptions": {
"target": "ES2020",
"module": "ESNext",
"moduleResolution": "bundler",
"strict": true,
"composite": true,
"declaration": true,
"declarationMap": true,
"skipLibCheck": true
},
"references": [
{ "path": "./packages/core" },
{ "path": "./packages/ui" },
{ "path": "./apps/web" }
],
"files": []
}
// packages/core/tsconfig.json
{
"extends": "../../tsconfig.json",
"compilerOptions": {
"outDir": "./dist",
"rootDir": "./src"
},
"include": ["src/**/*"]
}
总结:一份好的
tsconfig.json是 TS 项目的基石。核心原则:strict: true起步、moduleResolution: "bundler"适配现代打包器、paths配合打包器别名、skipLibCheck: true提升编译性能。
10.1 渐进式迁移策略
从 JavaScript 迁移到 TypeScript 不需要一步到位。以下是经过验证的分阶段策略:
整体路线图
JS 项目 → 加 tsconfig(allowJs)→ 迁移工具函数 → 迁移核心模块 → 迁移组件 → 开启 strict → 清理 @ts-ignore
阶段一:基础设施搭建(0 成本)
1. 安装依赖
npm install --save-dev typescript @types/node
2. 创建宽松 tsconfig.json
{
"compilerOptions": {
"target": "ES2020",
"module": "ESNext",
"moduleResolution": "bundler",
"allowJs": true,
"checkJs": false,
"noEmit": true,
"strict": false,
"esModuleInterop": true,
"skipLibCheck": true,
"resolveJsonModule": true,
"jsx": "react-jsx"
},
"include": ["src/**/*"],
"exclude": ["node_modules"]
}
关键:
allowJs: true+noEmit: true+strict: false确保不产生任何编译错误,先让 TypeScript 进入项目。
3. 增加一条 npm script
{
"scripts": {
"type-check": "tsc --noEmit"
}
}
阶段二:按优先级分步迁移
迁移优先级 —— 从内向外、从底层到上层:
| 优先级 | 目标 | 原因 |
|---|---|---|
| 1 | /types/ /constants/ | 纯数据定义,改动风险最低 |
| 2 | /utils/ /helpers/ /api/ | 工具函数,被依赖面广,先定类型 |
| 3 | /hooks/ /services/ /store/ | 业务逻辑层,依赖工具函数的类型 |
| 4 | /components/ /pages/ | UI 层,依赖以上所有层 |
具体操作
# 逐文件重命名(手动,推荐)
mv src/utils/format.js src/utils/format.ts
# 批量重命名(确认无副作用后用)
find src -name "*.js" -exec sh -c 'mv "$1" "${1%.js}.ts"' _ {} \;
find src -name "*.jsx" -exec sh -c 'mv "$1" "${1%.jsx}.tsx"' _ {} \;
每迁移一个文件的标准流程
- 将
.js重命名为.ts - 运行
npm run type-check - 根据报错逐一修复
- 如果有难以快速修复的错误,用
// @ts-ignore临时跳过,标记 TODO
阶段三:逐步收紧配置
// 第一步:开启声明检查
{ "compilerOptions": { "noImplicitAny": true } }
// 第二步:开启 null 检查
{ "compilerOptions": { "strictNullChecks": true } }
// 第三步:全面 strict
{ "compilerOptions": { "strict": true } }
// 第四步:开启未使用变量检查
{ "compilerOptions": { "noUnusedLocals": true, "noUnusedParameters": true } }
阶段四:清理技术债务
# 统计项目中 @ts-ignore 数量
grep -r "@ts-ignore" src/ | wc -l
# 逐版本减少 @ts-ignore 数量,目标归零
10.2 迁移中常见问题及解法
问题 1:大量的 any
// Before:迁移初期
const data: any = fetchData();
const result: any = process(data);
// After:逐步收紧
const data: ApiResponse = fetchData(); // 加具体类型
const result: ProcessedData = process(data); // 链式收紧
问题 2:第三方库无类型
# 1. 优先找 DefinitelyTyped 包
npm install --save-dev @types/some-library
# 2. 没有的话在 src/ 下建 types/ 目录声明
# src/types/some-library.d.ts
declare module "some-library" {
export function doSomething(input: string): number;
export const config: any;
}
# 3. 实在不行用 any
declare module "some-library"; // 全部 any
问题 3:Webpack / Vite 别名不生效
// tsconfig.json 中配置 paths 只影响类型检查
// 还需要在打包器中同步配置
// Vite(vite.config.ts)
import { resolve } from "path";
export default {
resolve: {
alias: { "@": resolve(__dirname, "src") }
}
};
// Webpack(webpack.config.js)
module.exports = {
resolve: {
alias: { "@": path.resolve(__dirname, "src") }
}
};
问题 4:模块导入错误
// CommonJS 模块在 TS 中可能报错
import express from "express"; // 可能报 "无法解析"
// 解法:开启 esModuleInterop
// tsconfig.json → { "compilerOptions": { "esModuleInterop": true } }
10.3 迁移 CheckList
- 安装 TypeScript 和基础 @types
- 创建 tsconfig.json(allowJs + noEmit + strict: false)
- 添加
type-checknpm script - 迁移
/types/和/constants/ - 迁移
/utils/和/helpers/ - 迁移 API 请求层
- 迁移自定义 Hooks / Composables
- 迁移 Store(Redux / Pinia / Zustand)
- 迁移组件(从叶节点组件开始)
- 迁移页面
- 开启
strict: true - 清理所有
@ts-ignore - CI 中加入
tsc --noEmit检查
经验法则:一个 50 个文件的中型 React 项目,全职迁移大约需要 3-5 天。不要试图一次性迁移所有文件,每次迁移一个模块,提交一次,确保随时可回滚。
11.1 类型错误排查技巧
技巧 1:鼠标悬停查看类型
在 VS Code 中,将鼠标悬停在变量、函数、导入上,会显示完整的类型信息。这是最基础的调试手段。
// 悬停在 fetchData 上
const data = fetchData(); // 显示:const data: ApiResponse<User> | null
技巧 2:使用 typeof 获取类型
当不确定某个值的类型时:
const user = { id: 1, name: "Alice" };
type UserType = typeof user; // { id: number; name: string }
// 获取函数返回值类型
function fetchUser() { return { id: 1, name: "Alice" }; }
type FetchUserReturn = ReturnType<typeof fetchUser>;
技巧 3:使用 // @ts-expect-error 测试类型
// 测试某个调用应该报错
function add(a: number, b: number): number {
return a + b;
}
// @ts-expect-error 期望这里报错
add("1", 2); // ✅ 如果这里不报错,测试会失败
// @ts-expect-error 期望这里报错
add(1, 2, 3); // ✅ 参数过多
技巧 4:类型断言调试
// 临时断言为 any 来绕过类型检查
const result = process(data as any);
// 更安全的:断言为 unknown 再转换
const result = process(data as unknown as MyType);
技巧 5:使用类型守卫缩小范围
// 当 TypeScript 无法推断类型时
function process(value: unknown) {
if (typeof value === "string") {
// 这里 value 是 string
console.log(value.toUpperCase());
}
if (Array.isArray(value)) {
// 这里 value 是 any[]
console.log(value.length);
}
}
11.2 常见编译错误及修复
错误 1:Property 'xxx' does not exist on type '...'
// 错误代码
interface User {
name: string;
email: string;
}
const user: User = { name: "Alice", email: "alice@example.com" };
console.log(user.age); // ❌ Property 'age' does not exist on type 'User'
// 修复方案
// 1. 检查拼写错误
console.log(user.name); // ✅
// 2. 如果确实需要可选属性,扩展接口
interface UserWithAge extends User {
age?: number;
}
// 3. 使用索引签名
interface User {
name: string;
email: string;
[key: string]: any; // 允许任意额外属性
}
错误 2:Type 'X' is not assignable to type 'Y'
// 错误代码
interface ButtonProps {
variant: "primary" | "secondary";
}
const props: ButtonProps = {
variant: "danger" // ❌ Type '"danger"' is not assignable to type '"primary" | "secondary"'
};
// 修复方案
// 1. 修正值
const props: ButtonProps = { variant: "primary" }; // ✅
// 2. 扩展类型
type ButtonVariant = "primary" | "secondary" | "danger";
interface ButtonProps {
variant: ButtonVariant;
}
// 3. 使用类型断言(谨慎)
const props = { variant: "danger" } as ButtonProps; // ⚠️ 可能隐藏真正问题
错误 3:Object is possibly 'null' or 'undefined'
// 错误代码
const element = document.getElementById("app");
element.classList.add("loaded"); // ❌ Object is possibly 'null'
// 修复方案
// 1. 使用可选链
element?.classList.add("loaded");
// 2. 使用非空断言(如果确定不为 null)
element!.classList.add("loaded");
// 3. 使用条件判断
if (element) {
element.classList.add("loaded");
}
// 4. 使用默认值
const element = document.getElementById("app") ?? document.body;
错误 4:Cannot find module 'xxx' or its corresponding type declarations
// 错误代码
import myLib from "my-lib"; // ❌ Cannot find module 'my-lib'
// 修复方案
// 1. 安装类型声明
npm install --save-dev @types/my-lib
// 2. 创建声明文件
// src/types/my-lib.d.ts
declare module "my-lib";
// 3. 如果使用路径别名,检查 tsconfig.json 的 paths
{
"compilerOptions": {
"paths": {
"my-lib": ["./src/libs/my-lib"]
}
}
}
错误 5:This expression is not callable. Type '...' has no call signatures
// 错误代码
const obj = { name: "test" };
obj(); // ❌ This expression is not callable
// 修复方案
// 检查是否误将对象当作函数调用
// 正确的调用方式
console.log(obj.name);
11.3 性能优化技巧
技巧 1:使用 interface 而非 type 进行扩展
// ✅ 性能更好,更清晰
interface User {
id: number;
name: string;
}
interface Admin extends User {
permissions: string[];
}
// ❌ 虽然可行,但性能稍差
type User = {
id: number;
name: string;
};
type Admin = User & {
permissions: string[];
};
技巧 2:避免深层嵌套类型
// ❌ 深层嵌套,类型检查慢
type DeepNested = {
level1: {
level2: {
level3: {
value: string;
};
};
};
};
// ✅ 扁平化
interface Level3 {
value: string;
}
interface Level2 {
level3: Level3;
}
interface Level1 {
level2: Level2;
}
技巧 3:使用 Omit 而非重新定义
// ✅ 更高效
interface User {
id: number;
name: string;
email: string;
createdAt: string;
}
type UserForm = Omit<User, "id" | "createdAt">;
// ❌ 重复定义
interface UserForm {
name: string;
email: string;
}
技巧 4:启用 skipLibCheck
{
"compilerOptions": {
"skipLibCheck": true // 跳过 node_modules 中的类型检查,显著提升编译速度
}
}
11.4 调试工具
1. TypeScript Playground
在线调试 TypeScript 代码:www.typescriptlang.org/play
2. tsc --noEmit --diagnostics
# 查看编译诊断信息
npx tsc --noEmit --diagnostics
# 输出示例
Files: 124
Lines: 23456
Nodes: 67890
Identifiers: 34567
Symbols: 45678
Types: 12345
Memory used: 123456 KB
I/O read: 0.01s
I/O write: 0.00s
Parse time: 0.12s
Bind time: 0.05s
Check time: 0.89s
Emit time: 0.00s
Total time: 1.06s
3. tsc --explainFiles
# 查看哪些文件被包含在编译中
npx tsc --explainFiles
4. 使用 tsserver 日志
# 在 VS Code 中启用 TypeScript 服务器日志
# 1. 打开命令面板 (Cmd+Shift+P)
# 2. 输入 "TypeScript: Open TS Server log"
11.5 常见陷阱
陷阱 1:过度使用 any
// ❌ 过度使用 any 失去类型安全
function process(data: any): any {
return data.someField.someMethod();
}
// ✅ 逐步收紧类型
function process<T>(data: T): ProcessedResult<T> {
// 具体实现
}
陷阱 2:忽略 strictNullChecks
// ❌ 未开启 strictNullChecks
function getLength(str: string): number {
return str.length; // 如果 str 是 null,运行时崩溃
}
// ✅ 开启 strictNullChecks
function getLength(str: string | null): number {
if (!str) return 0;
return str.length;
}
陷阱 3:混淆 interface 和 type
| 特性 | interface | type |
|---|---|---|
| 扩展 | extends | &(交叉类型) |
| 实现 | 可以被类实现 implements | 不能直接被类实现 |
| 声明合并 | ✅ 支持 | ❌ 不支持 |
| 性能 | 通常更好 | 稍差 |
| 适用场景 | 对象形状、类契约 | 联合类型、元组、映射类型 |
陷阱 4:忽略第三方库版本兼容
{
"dependencies": {
"react": "^18.2.0"
},
"devDependencies": {
"@types/react": "^18.2.0" // 必须与 react 版本匹配
}
}
总结:TypeScript 的调试核心是理解类型系统的报错信息。大多数错误都可以通过鼠标悬停、查看类型定义、使用类型工具来解决。遇到复杂问题时,逐步缩小范围、使用类型断言调试、查阅官方文档是有效策略。