TypeScript高级编程教程

教程简介

面向有基础的TypeScript高级编程教程,深入讲解泛型、条件类型、映射类型、模板字面量类型、装饰器、模块系统、声明文件、类型体操等进阶内容,配有与React/Vue集成和类型安全API客户端库实战项目。

TypeScript 高级编程教程

面向读者:已有 TypeScript 基础(了解接口、基础类型、函数类型等),希望深入掌握类型系统高级特性与工程实践的开发者。


目录

  1. 泛型深入
  2. 条件类型
  3. 映射类型
  4. 模板字面量类型
  5. 装饰器
  6. 模块系统
  7. 声明文件
  8. 类型体操
  9. 与 React 集成
  10. 与 Vue 集成
  11. 实战项目:类型安全的 API 客户端库

1. 泛型深入

1.1 泛型基础回顾

泛型允许我们编写可复用的组件,这些组件可以支持多种类型而不丢失类型信息。

function identity<T>(arg: T): T {
  return arg;
}

const str = identity("hello"); // 类型推断为 string
const num = identity(42);      // 类型推断为 number

1.2 泛型约束

使用 extends 关键字对泛型参数进行约束,确保传入的类型满足特定条件。

// 约束 T 必须包含 length 属性
interface HasLength {
  length: number;
}

function logLength<T extends HasLength>(arg: T): T {
  console.log(arg.length);
  return arg;
}

logLength("hello");       // ✅ string 有 length
logLength([1, 2, 3]);     // ✅ array 有 length
// logLength(123);        // ❌ number 没有 length

1.3 keyof 约束

结合 keyof 操作符,可以安全地访问对象属性。

function getProperty<T, K extends keyof T>(obj: T, key: K): T[K] {
  return obj[key];
}

const user = { name: "Alice", age: 30 };
const name = getProperty(user, "name"); // string
const age = getProperty(user, "age");   // number
// getProperty(user, "email");          // ❌ "email" 不在 keyof 中

1.4 泛型默认值

泛型参数可以设置默认类型,类似于函数参数的默认值。

interface ApiResponse<T = unknown> {
  data: T;
  status: number;
  message: string;
}

// 使用默认类型
const response1: ApiResponse = {
  data: "anything",
  status: 200,
  message: "ok",
};

// 显式指定类型
interface User {
  id: number;
  name: string;
}

const response2: ApiResponse<User> = {
  data: { id: 1, name: "Alice" },
  status: 200,
  message: "ok",
};

1.5 多类型参数

泛型可以接受多个类型参数,适用于需要关联多个类型的场景。

function merge<T, U>(obj1: T, obj2: U): T & U {
  return { ...obj1, ...obj2 };
}

const merged = merge({ name: "Alice" }, { age: 30 });
// 类型为 { name: string } & { age: number }

// 更实用的例子:创建带默认值的对象
function createRecord<K extends string, V>(
  key: K,
  value: V
): Record<K, V> {
  return { [key]: value } as Record<K, V>;
}

const record = createRecord("name", "Alice");
// 类型为 Record<"name", string>,即 { name: string }

1.6 泛型工具类型

TypeScript 内置了许多泛型工具类型:

// Partial - 所有属性变为可选
type PartialUser = Partial<User>;
// { id?: number; name?: string }

// Required - 所有属性变为必选
type RequiredUser = Required<PartialUser>;

// Readonly - 所有属性变为只读
type ReadonlyUser = Readonly<User>;

// Pick - 选取指定属性
type UserName = Pick<User, "name">;
// { name: string }

// Omit - 排除指定属性
type UserWithoutId = Omit<User, "id">;
// { name: string }

// Record - 构造键值对类型
type UserRoles = Record<string, string[]>;

2. 条件类型

2.1 基础条件类型

条件类型的语法类似于三元表达式:T extends U ? X : Y

type IsString<T> = T extends string ? true : false;

type A = IsString<string>;  // true
type B = IsString<number>;  // false
type C = IsString<"hello">; // true

2.2 分布式条件类型

当条件类型的 T 是联合类型时,条件类型会自动分布到每个成员上。

type ToArray<T> = T extends any ? T[] : never;

// 分布式特性:联合类型的每个成员分别计算
type Result = ToArray<string | number>;
// 等价于 ToArray<string> | ToArray<number>
// 即 string[] | number[]

// 如果不想分布,用方括号包裹
type ToArrayNonDist<T> = [T] extends [any] ? T[] : never;
type Result2 = ToArrayNonDist<string | number>;
// (string | number)[]

2.3 infer 关键字

infer 用于在条件类型中推断(提取)类型。

// 提取函数返回类型
type ReturnType<T> = T extends (...args: any[]) => infer R ? R : never;

type R1 = ReturnType<() => string>;           // string
type R2 = ReturnType<(x: number) => boolean>; // boolean

// 提取函数参数类型
type Parameters<T> = T extends (...args: infer P) => any ? P : never;

type P1 = Parameters<(a: string, b: number) => void>;
// [a: string, b: number]

// 提取 Promise 的内部类型
type UnwrapPromise<T> = T extends Promise<infer U> ? U : T;

type U1 = UnwrapPromise<Promise<string>>; // string
type U2 = UnwrapPromise<number>;          // number

// 递归提取嵌套 Promise
type DeepUnwrap<T> = T extends Promise<infer U> ? DeepUnwrap<U> : T;

type U3 = DeepUnwrap<Promise<Promise<Promise<string>>>>; // string

2.4 条件类型的高级应用

// 根据参数类型决定返回类型
type Flatten<T> = T extends Array<infer U> ? U : T;

type F1 = Flatten<string[]>;  // string
type F2 = Flatten<number>;    // number

// 过滤联合类型
type Exclude<T, U> = T extends U ? never : T;

type E1 = Exclude<"a" | "b" | "c", "a">;       // "b" | "c"
type E2 = Exclude<string | number | boolean, string>; // number | boolean

// 提取联合类型中满足条件的成员
type Extract<T, U> = T extends U ? T : never;

type Ex1 = Extract<"a" | "b" | "c", "a" | "f">; // "a"

3. 映射类型

3.1 基础映射类型

映射类型基于索引签名语法,可以将现有类型的所有属性转换为新类型。

type OptionsFlags<T> = {
  [K in keyof T]: boolean;
};

interface Features {
  darkMode: () => void;
  notifications: () => void;
}

type FeatureFlags = OptionsFlags<Features>;
// {
//   darkMode: boolean;
//   notifications: boolean;
// }

3.2 修饰符操作

可以在映射类型中添加或移除 readonly? 修饰符。

// 移除可选修饰符
type Concrete<T> = {
  [K in keyof T]-?: T[K];
};

type MaybeUser = {
  id?: number;
  name?: string;
};

type DefiniteUser = Concrete<MaybeUser>;
// { id: number; name: string }

// 添加只读修饰符
type Frozen<T> = {
  readonly [K in keyof T]: T[K];
};

// 移除只读修饰符
type Mutable<T> = {
  -readonly [K in keyof T]: T[K];
};

3.3 键名重映射(as 子句)

TypeScript 4.1 引入了键名重映射,允许在映射过程中修改键名。

// 给所有属性添加前缀
type Getters<T> = {
  [K in keyof T as `get${Capitalize<string & K>}`]: () => T[K];
};

interface Person {
  name: string;
  age: number;
}

type PersonGetters = Getters<Person>;
// {
//   getName: () => string;
//   getAge: () => number;
// }

// 过滤属性
type RemoveByKind<T, Kind> = {
  [K in keyof T as T[K] extends Kind ? never : K]: T[K];
};

type RemoveMethods<T> = RemoveByKind<T, Function>;

interface Mixed {
  id: number;
  name: string;
  greet: () => void;
}

type DataOnly = RemoveMethods<Mixed>;
// { id: number; name: string }

3.4 内置映射类型

TypeScript 提供了多个内置映射类型:

interface User {
  name: string;
  age: number;
  email: string;
}

// Partial<T> - 所有属性可选
// 实现原理:
type MyPartial<T> = {
  [K in keyof T]?: T[K];
};

// Required<T> - 所有属性必选
type MyRequired<T> = {
  [K in keyof T]-?: T[K];
};

// Readonly<T> - 所有属性只读
type MyReadonly<T> = {
  readonly [K in keyof T]: T[K];
};

// Pick<T, K> - 选取属性
type MyPick<T, K extends keyof T> = {
  [P in K]: T[P];
};

// Record<K, V> - 构造类型
type MyRecord<K extends keyof any, V> = {
  [P in K]: V;
};

4. 模板字面量类型

4.1 基础模板字面量类型

模板字面量类型基于字符串字面量类型,使用类似模板字符串的语法。

type Greeting = `Hello, ${string}!`;

const g1: Greeting = "Hello, World!"; // ✅
const g2: Greeting = "Hello, Alice!"; // ✅
// const g3: Greeting = "Hi, World!"; // ❌

// 联合类型的分布式组合
type Color = "red" | "blue" | "green";
type Size = "sm" | "md" | "lg";

type ColorSize = `${Color}-${Size}`;
// "red-sm" | "red-md" | "red-lg" |
// "blue-sm" | "blue-md" | "blue-lg" |
// "green-sm" | "green-md" | "green-lg"

4.2 内置字符串操作类型

type Upper = Uppercase<"hello">;     // "HELLO"
type Lower = Lowercase<"HELLO">;     // "hello"
type Cap = Capitalize<"hello">;      // "Hello"
type Uncap = Uncapitalize<"Hello">;  // "hello"

// 组合使用
type EventName<T extends string> = `on${Capitalize<T>}`;
type ClickEvent = EventName<"click">; // "onClick"
type FocusEvent = EventName<"focus">; // "onFocus"

4.3 实际应用场景

// CSS 属性类型
type CSSUnit = "px" | "em" | "rem" | "%" | "vh" | "vw";
type CSSValue = `${number}${CSSUnit}`;

const width: CSSValue = "100px";
const height: CSSValue = "50vh";
// const bad: CSSValue = "abc"; // ❌

// 路径参数提取
type ExtractRouteParams<T extends string> =
  T extends `${string}:${infer Param}/${infer Rest}`
    ? Param | ExtractRouteParams<Rest>
    : T extends `${string}:${infer Param}`
      ? Param
      : never;

type Params = ExtractRouteParams<"/users/:userId/posts/:postId">;
// "userId" | "postId"

// 对象路径类型
type PathKeys<T, K extends keyof T = keyof T> =
  K extends string
    ? T[K] extends object
      ? `${K}` | `${K}.${PathKeys<T[K]>}`
      : `${K}`
    : never;

interface Config {
  db: {
    host: string;
    port: number;
  };
  api: {
    baseUrl: string;
    timeout: number;
  };
}

type ConfigPath = PathKeys<Config>;
// "db" | "db.host" | "db.port" | "api" | "api.baseUrl" | "api.timeout"

4.4 与条件类型结合

// 解析路径参数并构造参数对象类型
type ParsePathParams<T extends string> =
  T extends `${string}:${infer Param}/${infer Rest}`
    ? { [K in Param]: string } & ParsePathParams<Rest>
    : T extends `${string}:${infer Param}`
      ? { [K in Param]: string }
      : {};

type RouteParams = ParsePathParams<"/users/:userId/posts/:postId">;
// { userId: string } & { postId: string }

5. 装饰器

5.1 装饰器基础

装饰器是一种特殊的声明,可以附加到类声明、方法、访问器、属性或参数上。TypeScript 5.0+ 支持 TC39 Stage 3 标准装饰器。

// 启用 experimentalDecorators 或使用 TS 5.0+ 标准装饰器
// tsconfig.json: { "experimentalDecorators": true }

// 类装饰器
function Logger<T extends new (...args: any[]) => any>(
  target: T,
  context: ClassDecoratorContext
) {
  return class extends target {
    constructor(...args: any[]) {
      super(...args);
      console.log(`[${context.name}] 实例已创建`);
    }
  };
}

@Logger
class UserService {
  constructor() {
    console.log("UserService 初始化");
  }
}

const service = new UserService();
// 输出: [UserService] 实例已创建
// 输出: UserService 初始化

5.2 方法装饰器

function LogExecutionTime(
  target: Function,
  context: ClassMethodDecoratorContext
) {
  return function (this: any, ...args: any[]) {
    const start = performance.now();
    const result = target.call(this, ...args);
    const end = performance.now();
    console.log(`${String(context.name)} 执行耗时: ${(end - start).toFixed(2)}ms`);
    return result;
  };
}

function ValidateArgs(
  target: Function,
  context: ClassMethodDecoratorContext
) {
  return function (this: any, ...args: any[]) {
    for (const arg of args) {
      if (arg === null || arg === undefined) {
        throw new Error(`${String(context.name)}: 参数不能为 null 或 undefined`);
      }
    }
    return target.call(this, ...args);
  };
}

class Calculator {
  @LogExecutionTime
  @ValidateArgs
  add(a: number, b: number): number {
    // 模拟耗时操作
    let sum = 0;
    for (let i = 0; i < 1000000; i++) {
      sum += i;
    }
    return a + b;
  }
}

const calc = new Calculator();
calc.add(1, 2);
// 输出: add 执行耗时: x.xxms

5.3 属性装饰器与自动初始化

// 使用 TS 5.0 标准装饰器语法
function DefaultValue(value: any) {
  return function <T>(
    target: undefined,
    context: ClassFieldDecoratorContext
  ) {
    return function (initialValue: T) {
      return initialValue ?? value;
    };
  };
}

function Required<T>(
  target: undefined,
  context: ClassFieldDecoratorContext
) {
  return function (initialValue: T | undefined) {
    if (initialValue === undefined) {
      throw new Error(`${String(context.name)} 是必填字段`);
    }
    return initialValue;
  };
}

class UserForm {
  @DefaultValue("匿名用户")
  name: string = undefined as any;

  @DefaultValue(0)
  age: number = undefined as any;
}

const form = new UserForm();
console.log(form.name); // "匿名用户"
console.log(form.age);  // 0

5.4 装饰器工厂模式

// 创建可配置的装饰器
function Throttle(delay: number) {
  return function (
    target: Function,
    context: ClassMethodDecoratorContext
  ) {
    let lastCall = 0;
    return function (this: any, ...args: any[]) {
      const now = Date.now();
      if (now - lastCall >= delay) {
        lastCall = now;
        return target.call(this, ...args);
      }
    };
  };
}

function Memoize(
  target: Function,
  context: ClassMethodDecoratorContext
) {
  const cache = new Map<string, any>();
  return function (this: any, ...args: any[]) {
    const key = JSON.stringify(args);
    if (cache.has(key)) {
      return cache.get(key);
    }
    const result = target.call(this, ...args);
    cache.set(key, result);
    return result;
  };
}

class DataService {
  @Throttle(1000)
  handleScroll(position: number) {
    console.log(`滚动到位置: ${position}`);
  }

  @Memoize
  fibonacci(n: number): number {
    if (n <= 1) return n;
    return this.fibonacci(n - 1) + this.fibonacci(n - 2);
  }
}

6. 模块系统

6.1 ES 模块与 CommonJS

TypeScript 同时支持 ES 模块和 CommonJS 模块系统。

// ES 模块语法(推荐)
// math.ts
export function add(a: number, b: number): number {
  return a + b;
}

export default class MathHelper {
  static PI = 3.14159;
}

// 导入
import MathHelper, { add } from "./math";

// CommonJS 语法
// old-module.ts
module.exports = {
  helper: () => {},
};

// 导入 CommonJS 模块
import oldModule = require("./old-module");

6.2 模块解析策略

// tsconfig.json
{
  "compilerOptions": {
    // 模块系统
    "module": "ESNext",          // 推荐使用 ESNext
    "moduleResolution": "bundler", // 使用打包工具时推荐

    // 其他常用模块相关配置
    "esModuleInterop": true,      // 允许 CommonJS/ES 模块互操作
    "allowSyntheticDefaultImports": true,
    "resolveJsonModule": true,    // 允许导入 JSON 文件
    "isolatedModules": true       // 确保每个文件都可以独立编译
  }
}

6.3 动态导入

// 动态导入返回 Promise
async function loadModule() {
  const { add } = await import("./math");
  console.log(add(1, 2));
}

// 条件导入
async function getFormatter(format: string) {
  if (format === "json") {
    const { JsonFormatter } = await import("./json-formatter");
    return new JsonFormatter();
  } else {
    const { CsvFormatter } = await import("./csv-formatter");
    return new CsvFormatter();
  }
}

6.4 模块增强与声明合并

// 扩展第三方模块的类型
import "express";

declare module "express" {
  interface Request {
    user?: {
      id: string;
      role: string;
    };
  }
}

// 使用
import express from "express";
const app = express();
app.get("/", (req, res) => {
  console.log(req.user?.id); // 类型安全
});

// 声明自己的模块扩展
// types/env.d.ts
declare module "*.svg" {
  const content: string;
  export default content;
}

declare module "*.css" {
  const classes: { readonly [key: string]: string };
  export default classes;
}

7. 声明文件

7.1 声明文件基础

声明文件(.d.ts)用于描述 JavaScript 库的类型信息。

// types/my-lib.d.ts

// 声明一个模块
declare module "my-lib" {
  export function doSomething(input: string): number;
  export interface Options {
    verbose?: boolean;
    timeout?: number;
  }
  export default function main(options: Options): void;
}

// 使用
import main, { doSomething, Options } from "my-lib";

7.2 全局声明

// types/global.d.ts

// 扩展全局作用域
declare global {
  // 扩展 Window 接口
  interface Window {
    __APP_CONFIG__: {
      apiUrl: string;
      env: "development" | "production";
    };
  }

  // 全局工具函数
  function debugLog(message: string, ...args: any[]): void;

  // 全局类型
  type Nullable<T> = T | null;
  type Optional<T> = T | undefined;
}

// 必须导出一个空对象以使其成为模块
export {};

7.3 编写高质量声明文件

// types/advanced-lib.d.ts

declare module "advanced-lib" {
  // 命名空间与类型结合
  namespace AdvancedLib {
    interface Config {
      baseUrl: string;
      headers?: Record<string, string>;
      interceptors?: Interceptor[];
    }

    interface Interceptor {
      request?: (config: RequestConfig) => RequestConfig;
      response?: (response: Response) => Response;
    }

    interface RequestConfig {
      url: string;
      method: string;
      headers: Record<string, string>;
      data?: unknown;
    }

    // 泛型接口
    interface Client {
      get<T>(url: string): Promise<T>;
      post<T>(url: string, data: unknown): Promise<T>;
      put<T>(url: string, data: unknown): Promise<T>;
      delete<T>(url: string): Promise<T>;
    }
  }

  // 构造函数
  interface AdvancedLibConstructor {
    new (config: AdvancedLib.Config): AdvancedLib.Client;
    create(config: AdvancedLib.Config): AdvancedLib.Client;
  }

  const AdvancedLib: AdvancedLibConstructor;
  export default AdvancedLib;
}

7.4 三斜线指令

/// <reference path="./types/global.d.ts" />
/// <reference types="node" />

// 三斜线指令用于声明文件间的依赖关系
// 现代项目中更推荐使用 import/export 和 tsconfig.json 的 references

8. 类型体操

8.1 DeepPartial(深度可选)

type DeepPartial<T> = T extends object
  ? { [K in keyof T]?: DeepPartial<T[K]> }
  : T;

interface Config {
  db: {
    host: string;
    port: number;
    credentials: {
      username: string;
      password: string;
    };
  };
  cache: {
    ttl: number;
  };
}

type PartialConfig = DeepPartial<Config>;
// 所有嵌套属性都变为可选

8.2 DeepReadonly(深度只读)

type DeepReadonly<T> = T extends object
  ? { readonly [K in keyof T]: DeepReadonly<T[K]> }
  : T;

type ReadonlyConfig = DeepReadonly<Config>;
// 所有嵌套属性都变为只读

8.3 UnionToIntersection(联合类型转交叉类型)

type UnionToIntersection<U> =
  (U extends any ? (k: U) => void : never) extends (k: infer I) => void
    ? I
    : never;

type Union = { a: string } | { b: number } | { c: boolean };
type Intersection = UnionToIntersection<Union>;
// { a: string } & { b: number } & { c: boolean }

8.4 Tuple 类型操作

// 获取元组第一个元素类型
type Head<T extends readonly any[]> = T extends readonly [infer H, ...any[]]
  ? H
  : never;

type H1 = Head<[string, number, boolean]>; // string

// 获取元组最后一个元素类型
type Last<T extends readonly any[]> = T extends readonly [...any[], infer L]
  ? L
  : never;

type L1 = Last<[string, number, boolean]>; // boolean

// 元组反转
type Reverse<T extends readonly any[]> = T extends readonly [
  infer Head,
  ...infer Rest,
]
  ? [...Reverse<Rest>, Head]
  : [];

type R1 = Reverse<[1, 2, 3]>; // [3, 2, 1]

// 元组转联合
type TupleToUnion<T extends readonly any[]> = T[number];

type U1 = TupleToUnion<[string, number, boolean]>; // string | number | boolean

8.5 字符串类型操作

// 驼峰转短横线
type CamelToKebab<S extends string> =
  S extends `${infer Head}${infer Tail}`
    ? Tail extends Uncapitalize<Tail>
      ? `${Lowercase<Head>}${CamelToKebab<Tail>}`
      : `${Lowercase<Head>}-${CamelToKebab<Tail>}`
    : S;

type K1 = CamelToKebab<"backgroundColor">; // "background-color"
type K2 = CamelToKebab<"fontSize">;         // "font-size"

// 短横线转驼峰
type KebabToCamel<S extends string> =
  S extends `${infer Head}-${infer Tail}`
    ? `${Head}${Capitalize<KebabToCamel<Tail>>}`
    : S;

type C1 = KebabToCamel<"background-color">; // "backgroundColor"

8.6 类型安全的 builder 模式

interface BuilderState {
  host?: string;
  port?: number;
  database?: string;
  ssl?: boolean;
}

class DatabaseBuilder<T extends BuilderState = {}> {
  private state: Partial<BuilderState> = {};

  setHost(host: string): DatabaseBuilder<T & { host: string }> {
    this.state.host = host;
    return this as any;
  }

  setPort(port: number): DatabaseBuilder<T & { port: number }> {
    this.state.port = port;
    return this as any;
  }

  setDatabase(db: string): DatabaseBuilder<T & { database: string }> {
    this.state.database = db;
    return this as any;
  }

  setSSL(ssl: boolean): DatabaseBuilder<T & { ssl: boolean }> {
    this.state.ssl = ssl;
    return this as any;
  }

  build(
    this: DatabaseBuilder<Required<BuilderState>>
  ): BuilderState {
    return this.state as BuilderState;
  }
}

// 使用
const builder = new DatabaseBuilder();

// ❌ 编译错误:缺少必需字段
// builder.build();

// ✅ 所有字段都设置后才能 build
const config = builder
  .setHost("localhost")
  .setPort(5432)
  .setDatabase("mydb")
  .setSSL(true)
  .build();

9. 与 React 集成

9.1 组件 Props 类型

import React, { useState, useEffect, useCallback } from "react";

// 基础 Props 定义
interface ButtonProps {
  variant?: "primary" | "secondary" | "danger";
  size?: "sm" | "md" | "lg";
  disabled?: boolean;
  onClick?: (event: React.MouseEvent<HTMLButtonElement>) => void;
  children: React.ReactNode;
}

const Button: React.FC<ButtonProps> = ({
  variant = "primary",
  size = "md",
  disabled = false,
  onClick,
  children,
}) => {
  return (
    <button
      className={`btn btn-${variant} btn-${size}`}
      disabled={disabled}
      onClick={onClick}
    >
      {children}
    </button>
  );
};

9.2 泛型组件

// 类型安全的列表组件
interface ListProps<T> {
  items: T[];
  renderItem: (item: T, index: number) => React.ReactNode;
  keyExtractor: (item: T) => string;
  emptyMessage?: string;
}

function List<T>({
  items,
  renderItem,
  keyExtractor,
  emptyMessage = "暂无数据",
}: ListProps<T>) {
  if (items.length === 0) {
    return <div className="empty">{emptyMessage}</div>;
  }

  return (
    <ul>
      {items.map((item, index) => (
        <li key={keyExtractor(item)}>{renderItem(item, index)}</li>
      ))}
    </ul>
  );
}

// 使用
interface User {
  id: number;
  name: string;
  email: string;
}

function UserList({ users }: { users: User[] }) {
  return (
    <List
      items={users}
      keyExtractor={(user) => String(user.id)}
      renderItem={(user) => (
        <div>
          <strong>{user.name}</strong>
          <span>{user.email}</span>
        </div>
      )}
      emptyMessage="没有用户"
    />
  );
}

9.3 Hooks 类型

// 自定义 Hook 的类型
interface UseFetchResult<T> {
  data: T | null;
  loading: boolean;
  error: Error | null;
  refetch: () => Promise<void>;
}

function useFetch<T>(url: string): UseFetchResult<T> {
  const [data, setData] = useState<T | null>(null);
  const [loading, setLoading] = useState(true);
  const [error, setError] = useState<Error | null>(null);

  const fetchData = useCallback(async () => {
    setLoading(true);
    setError(null);
    try {
      const response = await fetch(url);
      if (!response.ok) throw new Error(`HTTP ${response.status}`);
      const json = await response.json();
      setData(json);
    } catch (err) {
      setError(err instanceof Error ? err : new Error(String(err)));
    } finally {
      setLoading(false);
    }
  }, [url]);

  useEffect(() => {
    fetchData();
  }, [fetchData]);

  return { data, loading, error, refetch: fetchData };
}

// 类型安全的状态管理 Hook
function useReducerTyped<S, A extends { type: string }>(
  reducer: (state: S, action: A) => S,
  initialState: S
): [S, React.Dispatch<A>] {
  return React.useReducer(reducer, initialState);
}

9.4 事件处理类型

// 常见事件类型
interface FormProps {
  onSubmit: (data: FormData) => void;
}

interface FormData {
  username: string;
  email: string;
  message: string;
}

const ContactForm: React.FC<FormProps> = ({ onSubmit }) => {
  const handleSubmit = (e: React.FormEvent<HTMLFormElement>) => {
    e.preventDefault();
    const formData = new FormData(e.currentTarget);
    onSubmit({
      username: formData.get("username") as string,
      email: formData.get("email") as string,
      message: formData.get("message") as string,
    });
  };

  const handleInputChange = (
    e: React.ChangeEvent<HTMLInputElement | HTMLTextAreaElement>
  ) => {
    console.log(e.target.name, e.target.value);
  };

  return (
    <form onSubmit={handleSubmit}>
      <input name="username" onChange={handleInputChange} />
      <input name="email" type="email" onChange={handleInputChange} />
      <textarea name="message" onChange={handleInputChange} />
      <button type="submit">发送</button>
    </form>
  );
};

10. 与 Vue 集成

10.1 Vue 3 组合式 API 类型

import { ref, computed, reactive, watch, onMounted } from "vue";

// ref 类型推断
const count = ref(0); // Ref<number>
const name = ref<string | null>(null); // Ref<string | null>

// reactive 类型
interface UserProfile {
  id: number;
  name: string;
  preferences: {
    theme: "light" | "dark";
    language: string;
  };
}

const profile = reactive<UserProfile>({
  id: 1,
  name: "Alice",
  preferences: {
    theme: "light",
    language: "zh-CN",
  },
});

// computed 类型
const displayName = computed<string>(() => {
  return profile.name.toUpperCase();
});

// watch 类型
watch(
  () => profile.name,
  (newName: string, oldName: string) => {
    console.log(`名字从 ${oldName} 变为 ${newName}`);
  }
);

10.2 Props 与 Emits 类型

import { defineComponent, PropType } from "vue";

// 使用 defineComponent 的类型推断
const UserCard = defineComponent({
  props: {
    user: {
      type: Object as PropType<{
        id: number;
        name: string;
        avatar?: string;
      }>,
      required: true,
    },
    showEmail: {
      type: Boolean,
      default: false,
    },
    tags: {
      type: Array as PropType<string[]>,
      default: () => [],
    },
  },
  emits: {
    // 带类型验证的事件
    "update:user": (user: { id: number; name: string }) => true,
    delete: (id: number) => true,
  },
  setup(props, { emit }) {
    // props.user 自动推断为正确的类型
    console.log(props.user.name);

    const handleDelete = () => {
      emit("delete", props.user.id);
    };

    return { handleDelete };
  },
});

10.3 <script setup> 类型

<script setup lang="ts">
import { ref, computed } from "vue";

// Props 类型定义
interface Props {
  title: string;
  items: Array<{
    id: number;
    label: string;
    done: boolean;
  }>;
  maxItems?: number;
}

const props = withDefaults(defineProps<Props>(), {
  maxItems: 100,
});

// Emits 类型定义
const emit = defineEmits<{
  "update:title": [value: string];
  "item-click": [id: number];
  delete: [id: number];
}>();

// 响应式数据
const filter = ref<"all" | "done" | "pending">("all");

const filteredItems = computed(() => {
  switch (filter.value) {
    case "done":
      return props.items.filter((item) => item.done);
    case "pending":
      return props.items.filter((item) => !item.done);
    default:
      return props.items;
  }
});

// 暴露给父组件
defineExpose({
  reset: () => {
    filter.value = "all";
  },
});
</script>

10.4 Provide/Inject 类型

import { provide, inject, InjectionKey, Ref } from "vue";

// 定义 InjectionKey
interface AppContext {
  theme: Ref<"light" | "dark">;
  locale: Ref<string>;
  toggleTheme: () => void;
}

const AppContextKey: InjectionKey<AppContext> = Symbol("AppContext");

// Provider 组件
function setupApp() {
  const theme = ref<"light" | "dark">("light");
  const locale = ref("zh-CN");

  const toggleTheme = () => {
    theme.value = theme.value === "light" ? "dark" : "light";
  };

  provide(AppContextKey, { theme, locale, toggleTheme });
}

// Consumer 组件
function useAppContext(): AppContext {
  const context = inject(AppContextKey);
  if (!context) {
    throw new Error("useAppContext 必须在 App 组件内使用");
  }
  return context;
}

11. 实战项目:类型安全的 API 客户端库

在这一节中,我们将综合运用前面学到的所有高级类型特性,构建一个生产级别的类型安全 API 客户端库。

11.1 项目结构

typed-api-client/
├── src/
│   ├── types.ts          # 核心类型定义
│   ├── client.ts         # 客户端实现
│   ├── interceptors.ts   # 拦截器
│   ├── middleware.ts      # 中间件
│   └── index.ts          # 入口文件
├── tsconfig.json
└── package.json

11.2 核心类型定义

// src/types.ts

// HTTP 方法
type HttpMethod = "GET" | "POST" | "PUT" | "DELETE" | "PATCH";

// 基础响应类型
interface ApiResponse<T = unknown> {
  data: T;
  status: number;
  statusText: string;
  headers: Record<string, string>;
}

// API 错误类型
interface ApiError {
  status: number;
  message: string;
  code?: string;
  details?: Record<string, unknown>;
}

// 请求配置
interface RequestConfig {
  baseURL?: string;
  headers?: Record<string, string>;
  timeout?: number;
  params?: Record<string, string | number>;
  signal?: AbortSignal;
}

// 路径参数提取
type ExtractPathParams<T extends string> =
  T extends `${string}:${infer Param}/${infer Rest}`
    ? { [K in Param]: string } & ExtractPathParams<Rest>
    : T extends `${string}:${infer Param}`
      ? { [K in Param]: string }
      : {};

// API 端点定义
interface EndpointDef<
  TMethod extends HttpMethod = HttpMethod,
  TPath extends string = string,
  TBody = unknown,
  TResponse = unknown,
  TQuery = unknown,
> {
  method: TMethod;
  path: TPath;
  body?: TBody;
  response: TResponse;
  query?: TQuery;
}

// API 路由表类型
type ApiRoutes = {
  [key: string]: EndpointDef;
};

// 从路由表推断请求参数
type InferRequestParams<T extends EndpointDef> =
  (T["body"] extends undefined | void ? {} : { body: T["body"] }) &
  (T["query"] extends undefined | void ? {} : { query: T["query"] }) &
  (ExtractPathParams<T["path"]> extends Record<string, never>
    ? {}
    : { params: ExtractPathParams<T["path"]> });

// 客户端接口
type TypedApiClient<T extends ApiRoutes> = {
  [K in keyof T]: (
    ...args: InferRequestParams<T[K]> extends Record<string, never>
      ? [config?: RequestConfig]
      : [params: InferRequestParams<T[K]>, config?: RequestConfig]
  ) => Promise<ApiResponse<T[K]["response"]>>;
};

11.3 客户端实现

// src/client.ts

interface ClientConfig {
  baseURL: string;
  headers?: Record<string, string>;
  timeout?: number;
  interceptors?: {
    request?: RequestInterceptor[];
    response?: ResponseInterceptor[];
  };
}

type RequestInterceptor = (
  config: InternalRequestConfig
) => InternalRequestConfig | Promise<InternalRequestConfig>;

type ResponseInterceptor = (
  response: ApiResponse
) => ApiResponse | Promise<ApiResponse>;

interface InternalRequestConfig extends RequestConfig {
  method: HttpMethod;
  url: string;
  body?: unknown;
}

class TypedHttpClient<T extends ApiRoutes> {
  private config: ClientConfig;
  private requestInterceptors: RequestInterceptor[] = [];
  private responseInterceptors: ResponseInterceptor[] = [];

  constructor(config: ClientConfig) {
    this.config = config;
  }

  // 添加拦截器
  addRequestInterceptor(interceptor: RequestInterceptor): void {
    this.requestInterceptors.push(interceptor);
  }

  addResponseInterceptor(interceptor: ResponseInterceptor): void {
    this.responseInterceptors.push(interceptor);
  }

  // 构建完整 URL
  private buildUrl(
    path: string,
    params?: Record<string, string>,
    query?: Record<string, string | number>
  ): string {
    let url = path;

    // 替换路径参数
    if (params) {
      for (const [key, value] of Object.entries(params)) {
        url = url.replace(`:${key}`, encodeURIComponent(value));
      }
    }

    // 添加查询参数
    if (query) {
      const searchParams = new URLSearchParams();
      for (const [key, value] of Object.entries(query)) {
        searchParams.append(key, String(value));
      }
      url += `?${searchParams.toString()}`;
    }

    return `${this.config.baseURL}${url}`;
  }

  // 执行请求
  async request<K extends keyof T>(
    route: T[K],
    params?: Record<string, string>,
    query?: Record<string, string | number>,
    body?: unknown,
    extraConfig?: RequestConfig
  ): Promise<ApiResponse<T[K]["response"]>> {
    let config: InternalRequestConfig = {
      method: route.method,
      url: this.buildUrl(route.path, params, query),
      headers: {
        "Content-Type": "application/json",
        ...this.config.headers,
        ...extraConfig?.headers,
      },
      timeout: extraConfig?.timeout ?? this.config.timeout,
      body,
      ...extraConfig,
    };

    // 执行请求拦截器
    for (const interceptor of this.requestInterceptors) {
      config = await interceptor(config);
    }

    // 发起请求
    const response = await this.fetchWithTimeout(config);

    // 执行响应拦截器
    let result: ApiResponse = {
      data: await response.json(),
      status: response.status,
      statusText: response.statusText,
      headers: Object.fromEntries(response.headers.entries()),
    };

    for (const interceptor of this.responseInterceptors) {
      result = await interceptor(result);
    }

    if (!response.ok) {
      throw {
        status: response.status,
        message: result.statusText,
        details: result.data,
      } as ApiError;
    }

    return result as ApiResponse<T[K]["response"]>;
  }

  // 带超时的 fetch
  private async fetchWithTimeout(
    config: InternalRequestConfig
  ): Promise<Response> {
    const controller = new AbortController();
    const timeoutId = config.timeout
      ? setTimeout(() => controller.abort(), config.timeout)
      : undefined;

    try {
      const response = await fetch(config.url, {
        method: config.method,
        headers: config.headers,
        body: config.body ? JSON.stringify(config.body) : undefined,
        signal: config.signal ?? controller.signal,
      });
      return response;
    } finally {
      if (timeoutId) clearTimeout(timeoutId);
    }
  }
}

// 创建类型安全的客户端工厂函数
function createApiClient<T extends ApiRoutes>(
  config: ClientConfig
): TypedApiClient<T> {
  const client = new TypedHttpClient<T>(config);

  return new Proxy({} as TypedApiClient<T>, {
    get(_target, prop: string) {
      const method = prop.toUpperCase() as HttpMethod;
      return (params?: any, config?: RequestConfig) => {
        return client.request(
          { method, path: prop, response: null } as any,
          params?.params,
          params?.query,
          params?.body,
          config
        );
      };
    },
  });
}

11.4 定义 API 路由并使用

// src/example.ts

// 1. 定义 API 路由表
interface MyApiRoutes extends ApiRoutes {
  getUser: EndpointDef<
    "GET",
    "/users/:userId",
    undefined,
    { id: number; name: string; email: string }
  >;

  createUser: EndpointDef<
    "POST",
    "/users",
    { name: string; email: string },
    { id: number; name: string; email: string }
  >;

  updateUser: EndpointDef<
    "PUT",
    "/users/:userId",
    { name?: string; email?: string },
    { id: number; name: string; email: string }
  >;

  deleteUser: EndpointDef<
    "DELETE",
    "/users/:userId",
    undefined,
    { success: boolean }
  >;

  listPosts: EndpointDef<
    "GET",
    "/posts",
    undefined,
    Array<{ id: number; title: string; body: string }>,
    { page?: number; limit?: number }
  >;

  getPostComments: EndpointDef<
    "GET",
    "/posts/:postId/comments",
    undefined,
    Array<{ id: number; body: string }>
  >;
}

// 2. 创建客户端实例
const api = createApiClient<MyApiRoutes>({
  baseURL: "https://api.example.com",
  timeout: 5000,
  headers: {
    Authorization: "Bearer your-token",
  },
});

// 3. 类型安全的 API 调用

// ✅ 正确使用 - 完整的类型提示和检查
const user = await api.getUser({
  params: { userId: "123" },
});
// user.data 的类型为 { id: number; name: string; email: string }

const newUser = await api.createUser({
  body: { name: "Alice", email: "alice@example.com" },
});

const posts = await api.listPosts({
  query: { page: 1, limit: 10 },
});

const comments = await api.getPostComments({
  params: { postId: "42" },
});

// ❌ 编译错误示例
// api.getUser({});                    // 缺少 params.userId
// api.createUser({ body: { name: 123 } }); // body 类型不匹配
// api.listPosts({ query: { page: "1" } }); // page 应为 number

11.5 拦截器使用

// 添加请求拦截器 - 自动附加 token
api.addRequestInterceptor((config) => {
  const token = localStorage.getItem("auth_token");
  if (token) {
    config.headers = {
      ...config.headers,
      Authorization: `Bearer ${token}`,
    };
  }
  return config;
});

// 添加响应拦截器 - 统一错误处理
api.addResponseInterceptor((response) => {
  if (response.status === 401) {
    // Token 过期,跳转登录
    window.location.href = "/login";
  }
  return response;
});

// 添加响应拦截器 - 日志记录
api.addResponseInterceptor((response) => {
  console.log(
    `[API] ${response.status} ${response.statusText}`,
    response.data
  );
  return response;
});

11.6 错误处理

// 类型安全的错误处理
async function safeApiCall<T>(
  fn: () => Promise<ApiResponse<T>>
): Promise<{ data: T; error: null } | { data: null; error: ApiError }> {
  try {
    const response = await fn();
    return { data: response.data, error: null };
  } catch (error) {
    const apiError: ApiError =
      error && typeof error === "object" && "status" in error
        ? (error as ApiError)
        : { status: 0, message: String(error) };
    return { data: null, error: apiError };
  }
}

// 使用
const result = await safeApiCall(() =>
  api.getUser({ params: { userId: "123" } })
);

if (result.error) {
  console.error(`请求失败: ${result.error.message}`);
} else {
  console.log(`用户名: ${result.data.name}`);
}

11.7 完整项目总结

这个实战项目综合运用了以下 TypeScript 高级特性:

特性 应用场景
泛型 路由表类型参数化、响应类型推断
条件类型 InferRequestParams 根据端点定义推断参数
映射类型 TypedApiClient 从路由表生成方法签名
模板字面量类型 路径参数提取 ExtractPathParams
infer 关键字 提取路径中的参数名
交叉类型 组合多种请求参数类型
索引访问类型 访问嵌套类型定义
Proxy 运行时动态生成方法

通过这些特性,我们实现了一个零样板代码的 API 客户端:只需定义路由表类型,所有 API 调用都自动获得完整的类型提示、参数检查和返回值类型推断。


总结

本教程涵盖了 TypeScript 类型系统的核心高级特性:

  1. 泛型 是构建可复用类型的基础,结合约束和默认值可以实现灵活的类型抽象。
  2. 条件类型 允许根据类型关系进行条件判断,配合 infer 可以提取和转换类型。
  3. 映射类型 可以批量转换对象类型的属性,是构建工具类型的关键。
  4. 模板字面量类型 将字符串操作引入类型系统,适用于路由、CSS 属性等场景。
  5. 装饰器 提供了声明式的元编程能力,适合日志、验证、缓存等横切关注点。
  6. 模块系统 是大型项目组织代码的基础,理解模块解析策略至关重要。
  7. 声明文件 让 TypeScript 与 JavaScript 生态无缝集成。
  8. 类型体操 是对以上所有特性的综合运用,展示了类型系统的图灵完备性。

掌握这些高级特性,你将能够编写出真正类型安全、可维护且表现力强的 TypeScript 代码。


进阶资源推荐

内容声明

本文内容为AI技术学习教程,仅供学习参考。如涉及技术问题,欢迎通过 xurj005@163.com 与我们交流。

目录