TS教程详细篇

17 阅读33分钟

TypeScript 教程:面向前端 JavaScript 开发者的实战指南

本文档系统、全面地介绍 TypeScript,聚焦前端开发场景(React / Vue 3),覆盖从入门到进阶的完整学习路径。所有示例均来自真实前端项目场景。

目录


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 类型示例前端常见场景
stringstring"hello"用户输入、API 返回的文本、DOM 文本内容
numbernumber42, 3.14, NaN价格、数量、坐标、尺寸、百分比
booleanbooleantrue, false开关状态、条件判断、加载状态
ArrayT[]Array<T>[1, 2, 3]列表数据、选项数组、搜索结果
Objectobject{ x: 1, y: 2 }配置对象、表单数据、API 响应
nullnullnull显式清空值
undefinedundefinedundefined未初始化变量、可选参数
Function(args) => returnType(x: number) => x * 2事件处理器、回调函数
SymbolsymbolSymbol("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 尽可能推断,只在以下情况显式注解:

  1. 函数参数(必须)
  2. 对象字面量需要更精确的类型时
  3. 变量声明与初始化分离时
  4. 需要明确表达意图时

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);    // ✅ 编译通过,但运行时可能报错

使用原则:尽量避免。仅在以下场景临时使用:

  1. 迁移旧 JS 代码时的过渡期
  2. 处理第三方库的无类型返回值
  3. 快速原型阶段

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 再转目标类型

最佳实践

  1. 优先使用 as 语法
  2. 非空断言 ! 只在确定不为 null/undefined 时使用
  3. 双重断言是最后的逃生舱,尽量用类型守卫替代

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 社区最常被问的问题之一。两者的核心差异:

特性interfacetype
声明合并(同名自动合并)✅ 支持❌ 不支持
继承/扩展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 响应的数据结构
  ├── 定义组件 PropsReact/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());
}

总结:类型守卫是处理前端"不确定性数据"的核心工具。通过 typeofinstanceofin、自定义守卫和判别联合,你可以让 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 的类型声明文件,只包含类型信息,不包含实现。它们用于:

  1. 为无类型的 JavaScript 库提供类型支持
  2. 声明全局变量和模块
  3. 扩展第三方库的类型
// 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 生态的基石。掌握 declaredeclare 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" 才能用 documentwindowconsole 等浏览器 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"' _ {} \;

每迁移一个文件的标准流程

  1. .js 重命名为 .ts
  2. 运行 npm run type-check
  3. 根据报错逐一修复
  4. 如果有难以快速修复的错误,用 // @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-check npm 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:混淆 interfacetype

特性interfacetype
扩展extends&(交叉类型)
实现可以被类实现 implements不能直接被类实现
声明合并✅ 支持❌ 不支持
性能通常更好稍差
适用场景对象形状、类契约联合类型、元组、映射类型

陷阱 4:忽略第三方库版本兼容

{
  "dependencies": {
    "react": "^18.2.0"
  },
  "devDependencies": {
    "@types/react": "^18.2.0"  // 必须与 react 版本匹配
  }
}

总结:TypeScript 的调试核心是理解类型系统的报错信息。大多数错误都可以通过鼠标悬停、查看类型定义、使用类型工具来解决。遇到复杂问题时,逐步缩小范围、使用类型断言调试、查阅官方文档是有效策略。