TS函数与类实战
# 03.TS函数与类实战
从函数重载到抽象类——TS 如何为 JS 的动态性穿上类型铠甲,让 OOP 回归安全。
# 1. 案例引入
# 1.1 一个"写了文档都没人看"的函数
// 运营同事要你做:传入数字返回 id 查询,传入对象返回批量查询
function query(params) {
if (typeof params === "number") {
return fetchByID(params);
} else {
return fetchByIDs(params.ids);
}
}
// 调用者不知道到底返回什么类型
// 代码联想也无法给出提示
# 1.2 函数重载的解法
function query(id: number): User;
function query(params: { ids: number[] }): User[];
function query(params: number | { ids: number[] }): User | User[] {
if (typeof params === "number") {
return fetchByID(params);
}
return fetchByIDs(params.ids);
}
// 现在 IDE 自动识别:
const user = query(42); // user: User
const users = query({ids:[1,2]}); // users: User[]
这就是本节要讲的——如何用 函数类型系统 + 类系统 写出类型安全的可维护代码。
# 2. 函数类型表达式
# 2.1 三种定义函数类型的方式
// 方式 1:类型别名
type Add = (a: number, b: number) => number;
// 方式 2:interface(调用签名)
interface Add2 {
(a: number, b: number): number;
}
// 方式 3:直接标注
const add: (a: number, b: number) => number = (a, b) => a + b;
// 推荐:方式 1 或 3 用于一般场景;方式 2 用于需要声明合并的函数类型
# 2.2 参数的可选与默认值
// 可选参数 → 自动加 undefined 到类型
function greet(name: string, greeting?: string): string {
return `${greeting || "Hello"}, ${name}`;
}
// 默认值参数 → 自动推断类型,且变成可选
function createUser(name: string, age = 18) {
// age: number(自动推断),调用时可选
}
// 剩余参数
function sum(...nums: number[]): number {
return nums.reduce((a, b) => a + b, 0);
}
sum(1, 2, 3, 4); // ✅
# 2.3 参数解构的类型标注
// 对象解构标注——注意类型标注写在解构后面
function draw({ x, y, color }: { x: number; y: number; color?: string }): void {
console.log(`Drawing at (${x}, ${y}) with ${color}`);
}
// 推荐:提前定义类型
interface Point {
x: number;
y: number;
}
function move(point: Point, dx: number, dy: number): Point {
return { x: point.x + dx, y: point.y + dy };
}
# 3. 函数重载
# 3.1 重载的语法规则
// 重载签名(仅声明,不实现)——可以有多个
function format(value: string): string;
function format(value: number): string;
function format(value: boolean): string;
// 实现签名(兼容所有重载签名)
function format(value: string | number | boolean): string {
if (typeof value === "string") return `"${value}"`;
if (typeof value === "number") return value.toFixed(2);
return value ? "true" : "false";
}
format("hello"); // string → IDE 显示对应重载签名
format(3.14159); // string
format(true); // string
# 3.2 重载的黄金法则
// 法则 1:实现签名必须兼容所有重载签名
function getData(id: number): User;
function getData(ids: number[]): User[];
// ✅ 好——实现签名用更泛化的类型
function getData(idOrIds: number | number[]): User | User[] { /* ... */ }
// ❌ 坏——实现签名太窄
// function getData(id: number): User | User[] { /* ... */ }
// 法则 2:优先使用联合类型而非重载(除非返回值形状天差地别)
// 能写 len(s: string | any[]): number → 就不要重载
# 3.3 重载在实际场景中的最佳实践
// 真实案例:表单提交——返回类型随参数变化
function submit<T>(data: T): Promise<T>;
function submit<T>(data: T, options: { retry: number }): Promise<{ result: T; retries: number }>;
function submit<T>(
data: T,
options?: { retry: number }
): Promise<T | { result: T; retries: number }> {
if (options) {
let retries = 0;
while (retries <= options.retry) {
try { return Promise.resolve({ result: data, retries }); }
catch { retries++; }
}
throw new Error("Failed after retries");
}
return Promise.resolve(data);
}
const a = submit({ name: "John" }); // Promise<{name:string}>
const b = submit({ name: "John" }, { retry: 3 }); // Promise<{result:{name:string},retries:number}>
# 4. this 参数声明
# 4.1 典型陷阱:回调中的 this
class Counter {
count = 0;
increment() {
this.count++;
}
}
const c = new Counter();
const fn = c.increment;
// fn(); // ❌ 运行时:this 为 undefined(严格模式)
# 4.2 TS 的 this 参数
class Counter {
count = 0;
// this 作为"假参数"——编译后消失,仅用于类型检查
increment(this: Counter) {
this.count++;
}
}
// 现在传递时的类型错误就会被捕获
// const fn: (this: Counter) => void = c.increment;
// fn(); // ❌ 类型错误:缺少 this 上下文
// 也可以在非类函数中指定 this
interface UIContext {
element: HTMLElement;
onClick: (this: UIContext, e: MouseEvent) => void;
}
# 4.3 三种绑定方式对比
class Button {
label = "Click";
// 方式 1:箭头函数属性(自动绑定 this)
onClick = () => {
console.log(this.label); // ✅ this 永远是 Button 实例
};
// 方式 2:显式 this 参数
onClick2(this: Button) {
console.log(this.label);
}
// 方式 3:bind 绑定(运行时,TS 检查不了)
}
// 选型:箭头函数属性最安全(但占用实例内存,非共享原型方法)
# 5. 类:字段与构造器
# 5.1 字段声明的三种位置
class User {
// 字段声明
name: string;
age: number;
// 带初始值(自动推断类型)
createdAt = new Date();
// 非空断言(绕过 definite assignment check)
id!: number;
constructor(name: string, age: number, id: number) {
this.name = name;
this.age = age;
this.id = id;
}
}
# 5.2 参数属性(Parameter Properties)
// 传统写法——重复声明
class OldStyle {
name: string;
age: number;
constructor(name: string, age: number) {
this.name = name;
this.age = age;
}
}
// 参数属性——简洁 50%
class NewStyle {
constructor(
public name: string, // 自动 this.name = name
public age: number // 自动 this.age = age
) {}
}
# 5.3 getters 与 setters
class Circle {
constructor(private _radius: number) {}
get radius(): number {
return this._radius;
}
set radius(value: number) {
if (value <= 0) throw new Error("半径必须 > 0");
this._radius = value;
}
get area(): number {
return Math.PI * this._radius ** 2;
}
}
const c = new Circle(5);
console.log(c.area); // 78.54... getter 像属性一样访问
c.radius = 10; // setter 触发校验
# 6. 访问修饰符
# 6.1 public / protected / private
class Animal {
public name: string; // 默认 public——任何地方可访问
protected age: number; // 自身 + 子类可访问
private _secret: string; // 仅自身可访问
constructor(name: string, age: number, secret: string) {
this.name = name;
this.age = age;
this._secret = secret;
}
}
class Dog extends Animal {
bark() {
console.log(this.name); // ✅ public
console.log(this.age); // ✅ protected——子类可访问
// console.log(this._secret); // ❌ private 不可
}
}
const dog = new Dog("旺财", 3, "xxx");
dog.name; // ✅
// dog.age; // ❌ protected 外部不可
// dog._secret; // ❌ private
注意:TS 的 private 只是编译时约束,运行时仍然可以通过 dog["_secret"] 访问。这是 TS 的类型擦除特性——让 OOP 在编码阶段安全即可。
# 6.2 JS 原生 # 私有字段(ES2022)
class SecureClass {
#password: string; // JS 原生私有字段——运行时也不可访问
private tsSecret: string; // TS 编译时私有
constructor(password: string, secret: string) {
this.#password = password;
this.tsSecret = secret;
}
}
// 选型:
// 需要真正运行时私密 → # 字段
// 仅需编译时 UML 式封装 → private
# 7. 继承与抽象类
# 7.1 extends 继承
class Vehicle {
constructor(protected speed: number) {}
move(): string {
return `以 ${this.speed} km/h 移动`;
}
}
class Car extends Vehicle {
constructor(speed: number, private brand: string) {
super(speed); // 子类 constructor 必须先调用 super()
}
override move(): string { // 显式声明 override(推荐)
return `${this.brand} ${super.move()}`;
}
}
# 7.2 abstract 抽象类
abstract class Shape {
abstract getArea(): number; // 抽象方法——子类必须实现
abstract perimeter: number; // 抽象只读属性
// 也可以有具体实现
describe(): string {
return `面积: ${this.getArea()}, 周长: ${this.perimeter}`;
}
}
class Rectangle extends Shape {
constructor(private w: number, private h: number) {
super();
}
getArea(): number { return this.w * this.h; }
get perimeter(): number { return 2 * (this.w + this.h); }
}
// const s = new Shape(); // ❌ 不能实例化抽象类
const r = new Rectangle(3, 4);
r.describe(); // "面积: 12, 周长: 14"
# 7.3 implements:实现接口
interface Flyable {
fly(): void;
altitude: number;
}
interface Swimmable {
swim(): void;
}
class Duck implements Flyable, Swimmable {
altitude = 0;
fly() { this.altitude += 10; }
swim() { console.log("swimming"); }
}
// TS 检查:Duck 必须同时实现 fly() 和 swim() + 包含 altitude 属性
# 8. 实战案例:状态机模式
// 用 TS 类和接口设计一个订单状态机
// 状态定义
type OrderState = "pending" | "paid" | "shipped" | "delivered" | "cancelled";
// 状态转移表(只允许合法转移)
const transitions: Record<OrderState, OrderState[]> = {
pending: ["paid", "cancelled"],
paid: ["shipped", "cancelled"],
shipped: ["delivered"],
delivered: [],
cancelled: []
};
abstract class OrderBase {
constructor(
protected id: string,
public state: OrderState,
protected createdAt: Date
) {}
protected canTransitionTo(target: OrderState): boolean {
return transitions[this.state].includes(target);
}
abstract transition(target: OrderState): void;
abstract getInfo(): string;
}
class Order extends OrderBase {
private updatedAt: Date;
constructor(id: string) {
super(id, "pending", new Date());
this.updatedAt = new Date();
}
override transition(target: OrderState): void {
if (!this.canTransitionTo(target)) {
throw new Error(`不允许从 ${this.state} 转到 ${target}`);
}
this.state = target;
this.updatedAt = new Date();
}
override getInfo(): string {
return `订单 ${this.id}: ${this.state} | 创建: ${this.createdAt.toISOString()}`;
}
}
const order = new Order("ORD-001");
order.transition("paid"); // ✅
// order.transition("shipped"); // 抛出异常(该状态不允许转移)
# 9. 速查表
| 特性 | 语法 | 关键说明 |
|---|---|---|
| 函数类型 | (a:number) => string | 箭头语法,返回类型在 => 后 |
| 调用签名 | { (a:number): string } | interface 形式 |
| 重载 | 多个签名 + 一个实现 | 实现签名必须兼容所有重载 |
| 可选参数 | x?: string | 自动加 \| undefined |
| this 参数 | (this: MyClass, ...) | 编译后移除,仅用于检查 |
| 参数属性 | constructor(public n: number) | 一行三用(字段+构造+赋值) |
| get/set | get x(): number | 像属性一样访问 |
| private | private x | 编译时私有,运行时可绕过 |
| protected | protected x | 自身 + 子类可访问 |
| abstract | abstract class/method | 不可实例化,子类必须实现 |
| override | override method() | 显式标记覆盖,防重构错误 |
| implements | class A implements I | 实现多个接口 |
下一篇:04.TS泛型编程实战
上次更新: 2026/06/24, 12:59:24