跳转至

第 3 章:函数与类

学习时间:3 小时 | 难度:⭐⭐ 初级 | 前置知识:第 2 章


本章概览

本章深入 TypeScript 的函数系统和面向对象编程。从函数重载到类的访问控制,再到 TypeScript 5.x 标准化的装饰器,你将掌握构建健壮 TypeScript 应用的 OOP 工具箱。

学习目标:

  • 掌握函数重载、可选参数、默认参数、rest 参数
  • 理解 this 类型标注与函数类型的协变/逆变
  • 掌握类的访问修饰符:publicprivateprotectedreadonly
  • 理解抽象类与接口的实现区别
  • 掌握 TypeScript 5.x 标准化装饰器(ES2022 Decorators)
  • 理解 satisfies 运算符和 const 类型参数

3.1 函数类型与重载

TypeScript
// ── 函数类型的完整写法 ──────────────────────────────
// 写法 1:类型别名
type Comparator<T> = (a: T, b: T) => number;

// 写法 2:接口(可调用签名)
interface Middleware {
  (req: Request, res: Response, next: () => void): void;
}

// ── 可选参数与默认参数 ──────────────────────────────
function createUser(
  name: string,
  email: string,
  role: "admin" | "user" = "user", // 默认值(同时隐式设为可选)
  age?: number                      // 可选参数(必须在必填参数后面)
): User {
  return { id: Math.random(), name, email, role, age };
}

interface User {
  id: number;
  name: string;
  email: string;
  role: string;
  age?: number;
}

// ── Rest 参数 ────────────────────────────────────────
function sum(...numbers: number[]): number {
  return numbers.reduce((acc, n) => acc + n, 0);
}
console.log(sum(1, 2, 3, 4, 5)); // 15

// ── 函数重载:同一函数名,不同参数类型 ─────────────
// 先写重载签名(只声明,不实现)
function parse(input: string): number;
function parse(input: number): string;
function parse(input: string[]): number[];

// 再写实现签名(必须兼容所有重载签名)
function parse(input: string | number | string[]): number | string | number[] {
  if (typeof input === "string") {
    return parseInt(input, 10);    // string → number
  }
  if (typeof input === "number") {
    return input.toString();       // number → string
  }
  return input.map((s) => parseInt(s, 10)); // string[] → number[]
}

// 调用时 TypeScript 根据参数类型选择正确的重载
const num = parse("42");     // 类型:number
const str = parse(42);       // 类型:string
const nums = parse(["1", "2"]); // 类型:number[]

// ── 泛型函数 ─────────────────────────────────────────
// 函数级泛型(不需要类级别时优先用函数泛型)
function first<T>(arr: T[]): T | undefined {
  return arr[0];
}

function zip<A, B>(arrA: A[], arrB: B[]): [A, B][] {
  return arrA.map((a, i) => [a, arrB[i]] as [A, B]);
}

const zipped = zip([1, 2, 3], ["a", "b", "c"]);
// zipped: [number, string][]

// ── this 类型 ────────────────────────────────────────
interface Counter {
  count: number;
  increment(): this; // 返回 this 支持链式调用
  decrement(): this;
  reset(): this;
}

class NumberCounter implements Counter {
  count = 0;

  increment(): this {
    this.count++;
    return this;
  }

  decrement(): this {
    this.count--;
    return this;
  }

  reset(): this {
    this.count = 0;
    return this;
  }
}

const c = new NumberCounter();
c.increment().increment().increment().decrement(); // count = 2(链式调用)

3.2 类(Class)

TypeScript
// ── 完整的 TypeScript 类示例 ────────────────────────
class BankAccount {
  // 属性声明与访问修饰符
  readonly accountId: string;              // 只读:初始化后不可改
  private balance: number;                 // 私有:只有类内部访问
  protected owner: string;                 // 受保护:子类可访问
  public currency: string;                 // 公开(默认)

  // 静态属性
  static interestRate = 0.05;

  // 构造函数简写(参数前加修饰符直接变属性)
  constructor(
    owner: string,
    private readonly bankName: string,     // private readonly 直接声明
    initialBalance: number = 0,
  ) {
    this.accountId = crypto.randomUUID();  // 生成唯一 ID
    this.owner = owner;
    this.balance = initialBalance;
    this.currency = "CNY";
  }

  // Getter / Setter
  get currentBalance(): number {
    return this.balance;
  }

  set deposit(amount: number) {
    if (amount <= 0) throw new Error("Deposit must be positive");
    this.balance += amount;
  }

  // 实例方法
  withdraw(amount: number): void {
    if (amount > this.balance) {
      throw new Error("Insufficient funds");
    }
    this.balance -= amount;
  }

  // 静态方法
  static calculateInterest(principal: number, years: number): number {
    return principal * this.interestRate * years;
  }

  // toString 用于调试
  toString(): string {
    return `[${this.bankName}] ${this.owner}: ${this.currency} ${this.balance}`;
  }
}

const account = new BankAccount("Alice", "招商银行", 10000);
account.deposit = 5000;          // 触发 setter
console.log(account.currentBalance); // 15000(getter)
account.withdraw(3000);
// account.balance = 0;          // ❌ 错误:private 不可外部访问

// ── 继承 ─────────────────────────────────────────────
class SavingsAccount extends BankAccount {
  private savingsRate: number;

  constructor(owner: string, bankName: string, initialBalance: number, savingsRate: number) {
    super(owner, bankName, initialBalance); // 必须先调用 super()
    this.savingsRate = savingsRate;
  }

  // 重写方法
  override withdraw(amount: number): void {
    // override 关键字:确保父类有此方法(防止拼写错误)
    console.log(`Savings penalty applied for ${this.owner}`);
    super.withdraw(amount); // 调用父类方法
  }

  addInterest(): void {
    const interest = this.currentBalance * this.savingsRate;
    this.deposit = interest; // 触发父类 setter
  }
}

3.3 抽象类与接口实现

TypeScript
// 抽象类:不能直接实例化,定义子类必须实现的契约
abstract class Animal {
  abstract makeSound(): string; // 抽象方法:子类必须实现
  abstract get name(): string;  // 抽象 getter

  // 非抽象方法:可以直接继承
  describe(): string {
    return `I am ${this.name} and I say: ${this.makeSound()}`;
  }

  sleep(): void {
    console.log(`${this.name} is sleeping...`);
  }
}

class Dog extends Animal {
  get name(): string { return "Dog"; }
  makeSound(): string { return "Woof!"; }
}

class Cat extends Animal {
  get name(): string { return "Cat"; }
  makeSound(): string { return "Meow!"; }
}

// const animal = new Animal(); // ❌ 错误:抽象类不能实例化
const dog = new Dog();
console.log(dog.describe()); // "I am Dog and I say: Woof!"

// 接口实现(implements)
interface Serializable {
  serialize(): string;
}

interface Validatable {
  validate(): boolean;
}

// 一个类可以实现多个接口
class FormData implements Serializable, Validatable {
  constructor(
    private email: string,
    private name: string
  ) {}

  serialize(): string {
    return JSON.stringify({ email: this.email, name: this.name });
  }

  validate(): boolean {
    return this.email.includes("@") && this.name.length >= 2;
  }
}

3.4 TypeScript 5.x 标准化装饰器

TypeScript 5.0 实现了 TC39 Stage 3 装饰器提案(与旧版 experimentalDecorators 不同):

TypeScript
// 需要在 tsconfig.json 中,target >= ES2022(新装饰器不需要 experimentalDecorators)

// ── 类装饰器 ─────────────────────────────────────────
function singleton<T extends { new(...args: unknown[]): object }>(
  BaseClass: T,
  _ctx: ClassDecoratorContext
) {
  let instance: InstanceType<T>;

  const newClass = class extends BaseClass {
    constructor(...args: unknown[]) {
      if (instance) return instance; // 如果已有实例,直接返回
      super(...args);
      instance = this as InstanceType<T>;
    }
  };

  return newClass as T;
}

@singleton
class DatabaseConnection {
  private static connectionCount = 0;

  constructor(public readonly url: string) {
    DatabaseConnection.connectionCount++;
    console.log(`Connection #${DatabaseConnection.connectionCount} created`);
  }
}

const db1 = new DatabaseConnection("postgres://...");
const db2 = new DatabaseConnection("postgres://...");
console.log(db1 === db2); // true(单例模式)

// ── 方法装饰器 ───────────────────────────────────────
function log(target: unknown, context: ClassMethodDecoratorContext) {
  const methodName = String(context.name);

  return function (this: unknown, ...args: unknown[]) {
    console.log(`[${methodName}] called with:`, args);
    const result = (target as Function).apply(this, args);
    console.log(`[${methodName}] returned:`, result);
    return result;
  };
}

function memoize(_target: unknown, context: ClassMethodDecoratorContext) {
  const cache = new Map<string, unknown>();

  return function (this: unknown, ...args: unknown[]) {
    const key = JSON.stringify(args);
    if (cache.has(key)) {
      return cache.get(key);
    }
    const result = (_target as Function).apply(this, args);
    cache.set(key, result);
    return result;
  };
}

class Calculator {
  @log
  add(a: number, b: number): number {
    return a + b;
  }

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

// ── 字段装饰器 ───────────────────────────────────────
function nonNegative(target: undefined, context: ClassFieldDecoratorContext) {
  return function (this: unknown, value: number): number {
    if (value < 0) throw new RangeError(`${String(context.name)} cannot be negative`);
    return value;
  };
}

class Product {
  @nonNegative  // 确保价格不为负数
  price: number = 0;

  constructor(price: number) {
    this.price = price; // 触发装饰器验证
  }
}

3.5 TypeScript 5.x 新特性速查

satisfies 运算符(TS 4.9+)

TypeScript
// 问题:as 断言会丢失精确类型,不加断言又无利用约束
// satisfies 两全其美:既检查约束,又保留精确类型

type Colors = Record<string, string | [number, number, number]>;

// ❌ 旧写法:id 丢失颜色精确类型
const palette1: Colors = {
  red:   [255, 0, 0],
  green: "#00ff00",
  blue:  [0, 0, 255],
};
// palette1.red 类型是 string | [number, number, number],无法用 map

// ✅ satisfies:检查是否满足 Colors 约束,但保留字面量类型
const palette2 = {
  red:   [255, 0, 0],
  green: "#00ff00",
  blue:  [0, 0, 255],
} satisfies Colors;

// palette2.red 类型仍然是 [number, number, number](精确元组!)
const [r, g, b] = palette2.red; // ✅ OK:TS 知道是三元素元组
// palette2.green 类型是 string
const upper = palette2.green.toUpperCase(); // ✅ OK:TS 知道是 string

const 类型参数(TS 5.0+)

TypeScript
// 问题:泛型类型推断有时过于宽泛
function route<T extends string>(path: T) {
  return { path, handler: () => {} };
}

const r1 = route("/users");
// r1.path 的类型是 string(太宽了)

// ✅ const 类型参数:推断为字面量类型
function routeConst<const T extends string>(path: T) {
  return { path, handler: () => {} };
}

const r2 = routeConst("/users");
// r2.path 的类型是 "/users"(字面量!)

// 对数组特别有用
function createTuple<const T extends readonly unknown[]>(items: T) {
  return items;
}

const t = createTuple([1, "hello", true]);
// t 的类型是 readonly [1, "hello", true](元组!而非 (number | string | boolean)[])

using 声明(TS 5.2+ / ES2025)

TypeScript
// 使用 Symbol.dispose 自动资源管理(类似 Python with / C# using)
class DatabaseConnection {
  constructor(public url: string) {
    console.log(`Connected to ${url}`);
  }

  query(sql: string) {
    return `Results for: ${sql}`;
  }

  [Symbol.dispose]() {
    console.log(`Disconnected from ${this.url}`);
  }
}

{
  using conn = new DatabaseConnection("postgres://localhost/mydb");
  const result = conn.query("SELECT * FROM users");
  console.log(result);
  // 代码块结束时自动调用 conn[Symbol.dispose]()
}
// 输出:
// Connected to postgres://localhost/mydb
// Results for: SELECT * FROM users
// Disconnected from postgres://localhost/mydb

🏋️ 本章练习

  1. 类设计:实现一个 EventEmitter<Events> 泛型类,使用参数为事件名映射到处理函数类型(如 { click: (x: number, y: number) => void }
  2. 装饰器实战:写 @validate 方法装饰器,在调用前验证参数非空
  3. satisfies 练习:为路由配置对象使用 satisfies 确保类型安全并保留精确路径类型

📌 本章小结

概念 关键点
函数重载 先写签名,再写实现;实现签名不对外暴露
访问修饰符 private/protected/public/readonly + 构造函数简写
抽象类 定义子类契约,可包含实现;abstract 不可实例化
装饰器 TS 5.0 标准化,无需 experimentalDecorators
satisfies 约束检查 + 保留精确类型(两全其美)
const 泛型 推断字面量/元组而非宽泛类型
using 自动资源管理(dispose pattern)

下一章:高级类型编程——泛型约束、条件类型、映射类型、infer、模板字面量类型。


TypeScript 5.8 · 2025