普通视图

发现新文章,点击刷新页面。
昨天 — 2025年9月13日首页

【TS 设计模式完全指南】TypeScript 装饰器模式的优雅之道

作者 烛阴
2025年9月13日 10:51

一、什么是装饰器模式?

装饰器模式(Decorator Pattern)是一种结构型设计模式,它允许你在不修改现有对象结构的情况下,动态地为对象添加新的功能。它通过将对象包装在一个“装饰器”对象中来实现,这个装饰器对象提供了与原对象相同的接口,并增加了额外的行为。

二、装饰器模式的核心组件

  1. 组件 (Component):定义了原始对象和装饰器对象的共同接口。客户端代码将通过这个接口与所有对象(无论是否被装饰)进行交互。
  2. 具体组件 (Concrete Component):这是我们要装饰的原始对象,它实现了组件接口。
  3. 基础装饰器 (Base Decorator):一个抽象类,它也实现了组件接口。它内部持有一个对另一个“组件”对象的引用(可以是具体组件,也可以是另一个装饰器)。它的主要职责是将所有请求转发给被包装的对象。
  4. 具体装饰器 (Concrete Decorator):继承自基础装饰器,负责为组件添加特定的新功能。它会在转发请求之前或之后执行自己的附加逻辑。

三、示例:打造一个多渠道通知器

3.1 定义组件接口 (Component)

这是所有通知器都必须遵守的契约。

// Component: 通知器的统一接口
interface INotifier {
    send(message: string): void;
}

3.2 创建具体组件 (Concrete Component)

这是我们的基础,最核心的通知功能。

// Concrete Component: 基础通知器,只打印到控制台
class BaseNotifier implements INotifier {
    send(message: string): void {
        console.log(`[站内信]:发送消息 - "${message}"`);
    }
}

3.3 创建基础装饰器 (Base Decorator)

这个抽象类是所有装饰器的父类,它负责“包装”和“代理”。

// Base Decorator: 装饰器基类
abstract class NotifierDecorator implements INotifier {
    protected wrappedNotifier: INotifier;

    constructor(notifier: INotifier) {
        this.wrappedNotifier = notifier;
    }

    // 将 send 请求委托给被包装的对象
    send(message: string): void {
        this.wrappedNotifier.send(message);
    }
}

3.4. 创建具体装饰器 (Concrete Decorators)

现在来制作我们的实际业务:发送短信和邮件

// Concrete Decorator 1: 短信通知装饰器
class SMSDecorator extends NotifierDecorator {
    send(message: string): void {
        super.send(message); // 首先调用原始的 send 方法
        console.log(`[短信]:发送消息 - "${message}"`);
    }
}

// Concrete Decorator 2: 邮件通知装饰器
class EmailDecorator extends NotifierDecorator {
    send(message: string): void {
        super.send(message); // 调用原始方法
        console.log(`[邮件]:发送消息 - "${message}"`);
    }
}

注意 super.send(message) 的调用,它确保了包装链上的原始功能得以执行。你也可以把自己的逻辑放在它前面,实现前置处理。

3.5 自由组合,动态增强!

现在,我们可以像搭积木一样,自由地组合这些功能。

const message = '您的订单已发货!';

// 1. 只需要一个基础通知器
console.log('--- 基础通知 ---');
const basicNotifier = new BaseNotifier();
basicNotifier.send(message);

console.log('\n' + '='.repeat(30) + '\n');

// 2. 我需要站内信 + 短信
console.log('--- 站内信 + 短信 ---');
let notifierWithSMS = new BaseNotifier();
notifierWithSMS = new SMSDecorator(notifierWithSMS);
notifierWithSMS.send(message);

console.log('\n' + '='.repeat(30) + '\n');

// 3. 我需要站内信 + 短信 + 邮件
console.log('--- 站内信 + 短信 + 邮件 ---');
let notifierWithAll = new BaseNotifier();
notifierWithAll = new SMSDecorator(notifierWithAll);
notifierWithAll = new EmailDecorator(notifierWithAll);
notifierWithAll.send(message);

console.log('\n' + '='.repeat(30) + '\n');

// 4. 还可以有更疯狂的组合:站内信 + 邮件 + 再次短信(如果业务需要)
console.log('--- 疯狂组合 ---');
let crazyNotifier: INotifier = new BaseNotifier();
crazyNotifier = new EmailDecorator(crazyNotifier);
crazyNotifier = new SMSDecorator(crazyNotifier);
crazyNotifier.send('系统即将维护,请注意!');

运行结果:

--- 基础通知 ---
[站内信]:发送消息 - "您的订单已发货!"

==============================

--- 站内信 + 短信 ---
[站内信]:发送消息 - "您的订单已发货!"
[短信]:发送消息 - "您的订单已发货!"

==============================

--- 站内信 + 短信 + 邮件 ---
[站内信]:发送消息 - "您的订单已发货!"
[短信]:发送消息 - "您的订单已发货!"
[邮件]:发送消息 - "您的订单已发货!"

==============================

--- 疯狂组合 ---
[站内信]:发送消息 - "系统即将维护,请注意!"
[邮件]:发送消息 - "系统即将维护,请注意!"
[短信]:发送消息 - "系统即将维护,请注意!"

为了方便大家学习和实践,本文的所有示例代码和完整项目结构都已整理上传至我的 GitHub 仓库。欢迎大家克隆、研究、提出 Issue,共同进步!

📂 核心代码与完整示例: GoF

总结

如果你喜欢本教程,记得点赞+收藏!关注我获取更多JavaScript/TypeScript开发干货

昨天以前首页

【TS 设计模式完全指南】用适配器模式优雅地“兼容”一切

作者 烛阴
2025年9月11日 17:10

一、什么是适配器模式?

适配器模式(Adapter Pattern)是一种结构型设计模式,它的核心思想是:将一个类的接口转换成客户端所期望的另一个接口。 这使得原本因接口不兼容而无法协同工作的类可以一起工作。

二、适配器模式的核心组件

  • 目标 (Target):这是客户端代码所期待和依赖的接口。在TypeScript中,我们通常用 interface 来定义它。
  • 被适配者 (Adaptee):这是已经存在的、但接口与目标不兼容的类或对象。它是“需要被适配”的角色。
  • 适配器 (Adapter):这是模式的核心。它实现了 目标 (Target) 接口,并在内部持有对 被适配者 (Adaptee) 的引用。当客户端调用适配器的方法时,适配器会将其转换为对被适配者方法的调用。

三、示例:构建一个支付适配器

3.1 定义目标接口 (Target)

这是我们系统内部期望的支付接口,简洁明了。

// Target: 我们系统期望的支付接口
interface PaymentProcessor {
    processPayment(amount: number): void;
}

我们的客户端代码会像这样使用它:

class Store {
    private paymentProcessor: PaymentProcessor;

    constructor(processor: PaymentProcessor) {
        this.paymentProcessor = processor;
    }

    purchaseItem(price: number) {
        console.log(`准备购买一件价格为 ${price} 的商品...`);
        this.paymentProcessor.processPayment(price);
    }
}

3.2 引入“不兼容”的被适配者 (Adaptee)

现在,我们需要集成两个第三方的支付服务:PayPalStripe。不幸的是,它们的API设计和我们的 PaymentProcessor 完全不同。

// Adaptee 1: Ios 的支付服务
class IosPaymentGateway {
    public sendPayment(total: number, currency: string): void {
        console.log(`Ios平台 支付了 ${currency} ${total}`);
    }
}

// Adaptee 2: android 的支付服务
class AndroidGateway {
    public submitTransaction(value: number, description: string): boolean {
        console.log(`android 平台 支付了 ${description}, 金额: ${value}`);
        return true; // 表示交易成功
    }
}
  • 可以看到ios平台和Android平台的支付参数不一致

3.3 创建适配器 (Adapter)

现在,轮到我们的主角——适配器登场了。我们将为 IosAndroid 分别创建一个适配器。

为 Ios 创建适配器:

// Adapter for Ios
class IosAdapter implements PaymentProcessor {
    private iosGateway: IosPaymentGateway;

    constructor() {
        this.iosGateway = new IosPaymentGateway();
    }

    processPayment(amount: number): void {
        // 将我们简单的 processPayment 调用,转换为复杂的 sendPayment 调用
        console.log('适配器正在转换 ios 支付请求...');
        this.iosGateway.sendPayment(amount, 'USD');
    }
}

为 Android 创建适配器:

// Adapter for Android
class AndroidAdapter implements PaymentProcessor {
    private androidGateway: AndroidGateway;

    constructor() {
        this.androidGateway = new AndroidGateway();
    }

    processPayment(amount: number): void {
        // 将 processPayment 调用,转换为 submitTransaction 调用
        console.log('适配器正在转换 android 支付请求...');
        this.androidGateway.submitTransaction(amount, '商品购买');
    }
}

3.4 将它们组合在一起

现在,我们的 Store 类可以无缝地使用任何一种支付方式,而完全不需要知道 IosAndroid 的内部细节。

// 使用 Ios 支付
const iosAdapter = new IosAdapter();
const storeWithPayPal = new Store(iosAdapter);
storeWithPayPal.purchaseItem(100);

console.log('\n' + '='.repeat(30) + '\n');

// 切换到 Android 支付,Store 类代码无需任何修改!
const androidAdapter = new AndroidAdapter();
const storeWithStripe = new Store(androidAdapter);
storeWithStripe.purchaseItem(75);

运行结果:

准备购买一件价格为 100 的商品...
适配器正在转换 ios 支付请求...
Ios平台 支付了 USD 100

==============================

准备购买一件价格为 75 的商品...
适配器正在转换 android 支付请求...
android 平台 支付了 商品购买, 金额: 75

为了方便大家学习和实践,本文的所有示例代码和完整项目结构都已整理上传至我的 GitHub 仓库。欢迎大家克隆、研究、提出 Issue,共同进步!

📂 核心代码与完整示例: GoF

总结

如果你喜欢本教程,记得点赞+收藏!关注我获取更多JavaScript/TypeScript开发干货

【TS 设计模式完全指南】从零到一:掌握TypeScript建造者模式,让你的对象构建链式优雅

作者 烛阴
2025年9月9日 20:02
一、 建造者模式解决了什么痛点? 建造者模式的核心思想是:将一个复杂对象的构建过程与其表示分离,使得同样的构建过程可以创建不同的表示。 简单来说,它让你能够一步步地创建复杂对象,并且可以精确控制每个部

【TS 设计模式完全指南】用工厂方法模式打造你的“对象生产线”

作者 烛阴
2025年9月8日 18:07

一、 什么是工厂模式?

工厂模式(Factory Pattern)是最常用的设计模式之一,它提供了一种创建对象的方式,使得创建对象的过程与使用对象的过程分离。

工厂模式提供了一种创建对象的方式,而无需指定要创建的具体类。

通过使用工厂模式,可以将对象的创建逻辑封装在一个工厂类中,而不是在客户端代码中直接实例化对象,这样可以提高代码的可维护性和可扩展性。

二、工厂模式的类型

2.1 简单工厂模式(Simple Factory Pattern)

  • 简单工厂模式不是一个正式的设计模式,但它是工厂模式的基础。它使用一个单独的工厂类来创建不同的对象,根据传入的参数决定创建哪种类型的对象。

2.2 工厂方法模式(Factory Method Pattern)

  • 工厂方法模式定义了一个创建对象的接口,但由子类决定实例化哪个类。工厂方法将对象的创建延迟到子类。

2.3 抽象工厂模式(Abstract Factory Pattern)

  • 抽象工厂模式提供一个创建一系列相关或互相依赖对象的接口,而无需指定它们具体的类。

三、 工厂方法模式

工厂方法模式定义了一个用于创建对象的接口 Creator (创建者),但让子类 ConcreteCreator (具体创建者) 决定实例化哪一个类 ConcreteProduct (具体产品)。工厂方法使一个类的实例化延迟到其子类。

示例: 未使用工厂方法之前

// 1. 产品接口 (IShape)
interface IShape {
    draw(): void;
}

// 2. 具体产品 (Concrete Products)
class Circle implements IShape {
    constructor(public radius: number) {}
    draw(): void {
        console.log(`绘制圆形,半径:${this.radius}`);
    }
}

class Rectangle implements IShape {
    constructor(public width: number, public height: number) {}
    draw(): void {
        console.log(`绘制矩形,宽:${this.width},高:${this.height}`);
    }
}

class Triangle implements IShape {
    constructor(public side1: number, public side2: number, public side3: number) {}
    draw(): void {
        console.log(`绘制三角形,边长:${this.side1},${this.side2},${this.side3}`);
    }
}

// 3. 客户端代码 (创建逻辑与使用耦合)
type ShapeType = 'circle' | 'rectangle' | 'triangle' | 'pentagon';


function createAndDrawShape(type: ShapeType, ...args: number[]): void {
    let shape: IShape | null = null;

    // 痛点所在:大量的 if-else 或 switch-case
    if (type === 'circle') {
        shape = new Circle(args[0]!);
    } else if (type === 'rectangle') {
        shape = new Rectangle(args[0]!, args[1]!);
    } else if (type === 'triangle') {
        shape = new Triangle(args[0]!, args[1]!, args[2]!);
    } else {
        throw new Error('不支持的图形类型!');
    }

    if (shape) {
        shape.draw();
    }
}

// 使用
createAndDrawShape('circle', 5);
createAndDrawShape('rectangle', 10, 20);
createAndDrawShape('triangle', 3, 4, 5);
createAndDrawShape('pentagon', 5); // 编译通过,运行时报错

使用工厂模式

1 定义抽象工厂 (Creator)

一个定义了工厂方法的接口或抽象类,它负责声明生产产品的方法。

// 4.1 抽象工厂接口 (Creator)
interface IShapeFactory {
    createShape(...args: number[]): IShape; // 这就是“工厂方法”
}

2 为每个具体产品创建具体工厂 (Concrete Creator) 现在,我们不再需要一个大而全的 createAndDrawShape 函数。每种图形都有自己专属的、能创建自己的工厂

// 4.2 具体工厂 (Concrete Creators)
class CircleFactory implements IShapeFactory {
    createShape(...args: number[]): IShape {
        console.log('圆形工厂:创建圆形中...');
        // 这里可以封装复杂的圆形创建和初始化逻辑
        return new Circle(args[0]!);
    }
}

class RectangleFactory implements IShapeFactory {
    createShape(...args: number[]): IShape {
        console.log('矩形工厂:创建矩形中...');
        // 这里可以封装复杂的矩形创建和初始化逻辑
        return new Rectangle(args[0]!, args[1]!);
    }
}

class TriangleFactory implements IShapeFactory {
    createShape(...args: number[]): IShape {
        console.log('三角形工厂:创建三角形中...');
        // 这里可以封装复杂的三角形创建和初始化逻辑
        return new Triangle(args[0]!, args[1]!, args[2]!);
    }
}

3 改造客户端代码 现在,客户端不再直接 new 具体图形,也不再需要庞大的 if-else。它只需要知道它需要哪种工厂,然后通过工厂去创建产品。

// 5. 客户端代码 (通过工厂来创建和使用)
function clientCode(factory: IShapeFactory, ...args: number[]): void {
    const shape = factory.createShape(...args); // 客户端只通过工厂接口来创建对象
    shape.draw();
}

// 使用工厂方法模式
console.log('\n--- 使用工厂方法模式 ---');
const circleFactory = new CircleFactory();
clientCode(circleFactory, 5); // 客户端传入具体的工厂实例

const rectangleFactory = new RectangleFactory();
clientCode(rectangleFactory, 10, 20);

const triangleFactory = new TriangleFactory();
clientCode(triangleFactory, 3, 4, 5);

// 新增一个 Pentagon 图形
// 我们只需要:
// 1. 新增 Pentagon 类实现 IShape
// 2. 新增 PentagonFactory 实现 IShapeFactory
// 无需修改任何已有的工厂或 clientCode 函数!
class Pentagon implements IShape {
    constructor(public sideLength: number) {}
    draw(): void {
        console.log(`绘制五边形,边长:${this.sideLength}`);
    }
}

class PentagonFactory implements IShapeFactory {
    createShape(...args: number[]): IShape {
        console.log('五边形工厂:创建五边形中...');
        return new Pentagon(args[0]!);
    }
}

console.log('\n--- 扩展新图形:五边形 ---');
const pentagonFactory = new PentagonFactory();
clientCode(pentagonFactory, 8); // 轻松扩展,无需改动旧代码!

解析:

  • 通过工厂方法模式,我们实现了创建逻辑与使用逻辑的解耦,大大提升了代码的可维护性和扩展性。新增一个图形,只需要新增一个类和一个工厂,无需改动已有的代码!

四、 工厂方法模式的核心角色

为了更清晰地理解其结构,我们通常将工厂方法模式分解为以下几个核心角色:

  1. Product (抽象产品):定义了工厂方法所创建的对象的接口。
    • 在我们的例子中是 IShape
  2. ConcreteProduct (具体产品):实现了 Product 接口的具体类。
    • 例如 Circle, Rectangle, Triangle, Pentagon
  3. Creator (抽象工厂):声明了工厂方法,该方法返回一个 Product 对象。它是工厂方法模式的核心。
    • 在我们的例子中是 IShapeFactory
  4. ConcreteCreator (具体工厂):重写抽象工厂中的工厂方法,以返回一个 ConcreteProduct 实例。
    • 例如 CircleFactory, RectangleFactory, TriangleFactory, PentagonFactory

五、抽象工厂模式

提供一个创建一系列相关或相互依赖对象的接口,而无需指定它们具体的类。抽象工厂模式旨在解决创建多个产品等级结构中的相关产品组合的问题。

你现在不仅需要创建图形(IShape),还需要创建文本框(ITextBox)。更复杂的是,你的应用需要支持多种主题(例如:LightTheme 和 DarkTheme)

  • Light 主题下,你需要 LightCircleLightSquareLightTextBox
  • Dark 主题下,你需要 DarkCircleDarkSquareDarkTextBox

这时候,如果用工厂方法模式,你需要 LightCircleFactoryDarkCircleFactoryLightSquareFactoryDarkSquareFactory 等一系列工厂,管理起来会非常混乱。而且,你很难保证一个主题下的所有产品都使用统一的视觉风格。这个时候就可以使用抽象工厂模式,把每个主题作为一个组合下的创建图形和创建文本框封装成一个抽象工厂接口。

// 1. 抽象工厂 (AbstractFactory)
// 定义一个接口,用于创建一系列相关产品(一个产品组合)
interface IUIFactory {
    createShape(): IShape;
    createTextBox(): ITextBox;
    // ...还可以有 createButton(), createSlider() 等
}

六、工厂方法 vs. 抽象工厂:核心对比

特性 工厂方法模式 (Factory Method) 抽象工厂模式 (Abstract Factory)
解决问题 创建单一产品等级结构中的对象。 创建多个产品等级结构中的相关或依赖对象组合
创建对象数量 每个具体工厂通常只创建一个具体产品。 每个具体工厂可以创建一整套相关的具体产品 (一个组合)。
关注点 如何为特定产品创建具体实例 (一个工厂一个产品)。 如何创建“产品组合” (一个工厂一组产品)。
扩展性 1 (新增产品类型) :新增产品类型时,需添加新的具体产品类和新的具体工厂类。 :新增产品类型(比如新增 ISlider)时,需要修改抽象工厂接口和所有具体工厂类。
扩展性 2 (新增产品组合) 不适用/差:无法很好地处理产品组合切换问题。 :新增一个产品组合(如新增主题)时,只需添加新的具体工厂类和具体产品类。无需修改抽象工厂接口和客户端。
实现方式 继承。抽象工厂由子类实现具体产品的创建。 组合。抽象工厂由客户端选择具体的工厂接口,该接口包含多个创建方法。
类图特点 一个抽象工厂 + 多个具体工厂,每个工厂有自己的工厂方法。 一个抽象工厂 + 多个具体工厂,每个工厂有多个工厂方法(每个方法创建一个不同等级的产品)。
何时使用 当你的类不知道它要创建哪个具体类的对象,或者希望子类来指定要创建的对象时。 当一个系统需要独立于其产品的创建、组合和表示时。当一个系统要由多个产品组合中的一个来配置时。

为了方便大家学习和实践,本文的所有示例代码和完整项目结构都已整理上传至我的 GitHub 仓库。欢迎大家克隆、研究、提出 Issue,共同进步!

📂 核心代码与完整示例: GoF

总结

如果你喜欢本教程,记得点赞+收藏!关注我获取更多JavaScript/TypeScript开发干货

【TS 设计模式完全指南】从“入门”到“劝退”,彻底搞懂单例模式

作者 烛阴
2025年9月7日 21:11

一、 单例模式是什么?

保证一个类仅有一个实例,并提供一个全局访问点来获取这个实例。

二、 经典单例的 TypeScript 实现

要实现一个单例模式,我们需要做到三点:

  1. 构造函数必须是私有的 (private constructor),防止外部通过 new 关键字随意创建实例。
  2. 类内部需要持有一个静态的、私有的自身实例。
  3. 提供一个公开的、静态的方法 (getInstance),用于获取这个唯一的实例。
class AppConfig {
  // 1. 持有私有的静态实例
  private static instance: AppConfig;

  private config: Record<string, any>;

  // 2. 将构造函数私有化
  private constructor() {
    console.log("读取配置文件... (这只会被打印一次)");
    this.config = {
      version: "1.0.0",
      server: "https://api.example.com",
    };
  }

  public getConfig(key: string): any {
    return this.config[key];
  }

  // 3. 提供公开的静态方法获取实例
  public static getInstance(): AppConfig {
    if (!AppConfig.instance) {
      AppConfig.instance = new AppConfig();
    }
    return AppConfig.instance;
  }
}

// ---- 如何使用 ----

// const errorConfig = new AppConfig(); // ❌ 错误: "AppConfig" 的构造函数是私有的。TS 在编译期就阻止了你!

const config1 = AppConfig.getInstance();
const config2 = AppConfig.getInstance();

console.log(config1 === config2); // true,它们是同一个实例!

const serverUrl = config1.getConfig("server");
console.log(`服务器地址: ${serverUrl}`);

这就是经典的懒汉式单例(Lazy Initialization)。只有在第一次调用 getInstance() 时,实例才会被创建。TypeScript 的 private constructor 更是为我们提供了编译时的安全保障!

三、“饿汉式” vs “懒汉式”

除了上面的懒汉式,还有一种实现方式叫饿汉式。它在类加载时就立即创建实例,不管你用不用。

class EagerAppConfig {
    // 在类加载时就直接创建实例
    private static readonly instance: EagerAppConfig = new EagerAppConfig();

    private constructor() {
        console.log('饿汉式:配置文件已加载!');
    }

    public static getInstance(): EagerAppConfig {
        return EagerAppConfig.instance;
    }
}

// 即使还没调用 getInstance,构造函数里的 log 也会被打印出来
//const eagerConfig = EagerAppConfig.getInstance();

两者对比:

  • 懒汉式
    • 优点:延迟加载,节省资源。如果一直用不到这个实例,就不会创建它。
    • 缺点:在多线程环境下需要处理同步问题(不过在 JS/TS 的单线程事件循环模型中,实例化本身的竞态条件不是主要问题)。第一次获取实例时会稍慢。
  • 饿汉式
    • 优点:实现简单,天生线程安全。获取实例速度快。
    • 缺点:类加载时就初始化,可能造成资源浪费,尤其当实例创建很耗时,但应用又不一定会使用它时。

在 TS/JS 环境中,由于其模块加载机制,我们还有一种更简洁的“单例”实现方式。

四、JS/TS特有的单例模式实现

ES6 模块有一个重要特性:模块内的代码只会在第一次被导入时执行一次。之后再 import 同一个模块,只会得到缓存的导出结果。我们可以利用这个特性,轻松实现一个单例。

// logger.ts
class Logger {
    private logs: string[] = [];

    constructor() {
        console.log('Logger 初始化了!(只会发生一次)');
    }

    public log(message: string) {
        const timestamp = new Date().toISOString();
        this.logs.push(`[${timestamp}] ${message}`);
        console.log(`[Logger]: ${message}`);
    }

    public printLogs() {
        console.log(this.logs);
    }
}

// 直接实例化并导出
export const logger = new Logger();
// 这样在任何地方 import { logger } 都会得到同一个实例

// ---- 在其他文件中使用 ----
// a.ts
import { logger } from './logger';
logger.log("模块 A 的消息");

// b.ts
import { logger } from './logger';
logger.log("模块 B 的消息");
logger.printLogs(); // 会打印出模块 A 和 B 的两条消息

这种方式代码量最少,也最直观。它实际上是一种饿汉式的实现,非常适合那些创建开销不大且必定会被用到的场景。对于绝大多数 TS/JS 应用的简单全局实例需求,这通常是最佳选择

为了方便大家学习和实践,本文的所有示例代码和完整项目结构都已整理上传至我的 GitHub 仓库。欢迎大家克隆、研究、提出 Issue,共同进步!

📂 核心代码与完整示例: GoF

总结

如果你喜欢本教程,记得点赞+收藏!关注我获取更多JavaScript/TypeScript开发干货

TypeScript 进阶必修课:解锁强大的内置工具类型(二)

作者 烛阴
2025年9月3日 16:51

核心工具类型(二)

1. Exclude<T, U>: 排除联合类型成员

  • 作用: 从联合类型 T 中排除所有可分配给 U 的成员。

  • 示例:

    type AllPossibleId = number | string | boolean | undefined;
    
    // 我只想要非数字的 ID 类型
    type NonNumericId = Exclude<AllPossibleId, number>;
    // 结果是:type NonNumericId = string | boolean | undefined;
    
    // 排除 null 和 undefined
    type ValidStatus = Exclude<"active" | "inactive" | null | undefined, null | undefined>;
    // 结果是:type ValidStatus = "active" | "inactive";
    

2. Extract<T, U>: 提取联合类型成员

  • 作用: 从联合类型 T 中提取所有可分配给 U 的成员。

  • 示例:

    type AllPossibleValue = string | number | boolean | object;
    
    // 我只想要可以被序列化为 JSON 的原始类型
    type JsonPrimitive = Extract<AllPossibleValue, string | number | boolean>;
    // 结果是:type JsonPrimitive = string | number | boolean;
    

3. NonNullable<T>: 移除 nullundefined

  • 作用: 从类型 T 中排除 nullundefined

  • 示例:

    type MaybeString = string | null | undefined;
    
    // 确定其不为空后,可以获取其非空类型
    type GuaranteedString = NonNullable<MaybeString>;
    // 结果是:type GuaranteedString = string;
    
    function processString(text: GuaranteedString) {
        console.log(text.length); // ✅ 不会报错,因为 text 保证不是 null 或 undefined
    }
    
    const myText: MaybeString = "Hello";
    if (myText !== null && myText !== undefined) {
        processString(myText); // myText 在这里被收窄为 string
    }
    

4. ReturnType<T>: 获取函数返回类型

  • 作用: 获取函数类型 T 的返回类型。

  • 示例:

    function getUserData(id: number, name: string) {
        return { id, name, isAdmin: id === 1 };
    }
    
    // 获取 getUserData 函数的返回类型
    type UserData = ReturnType<typeof getUserData>;
    /* 等同于:
    type UserData = {
        id: number;
        name: string;
        isAdmin: boolean;
    }
    */
    
    const currentUser: UserData = {
        id: 5,
        name: 'Sarah',
        isAdmin: false,
    };
    

5. Parameters<T>: 获取函数参数类型

  • 作用: 获取函数类型 T 的参数类型,以元组的形式。

  • 示例:

    function greet(firstName: string, lastName?: string): string {
        return `Hello, ${firstName} ${lastName || ""}`;
    }
    
    // 获取 greet 函数的参数类型元组
    type GreetParams = Parameters<typeof greet>;
    // 结果是:type GreetParams = [firstName: string, lastName?: string | undefined];
    
    
    function callGreet(...args: GreetParams) {
         // 可以在这里对参数进行一些处理
         return greet(...args);
    }
    
    console.log(callGreet("John", "Doe")); // "Hello, John Doe"
    console.log(callGreet("Jane")); // "Hello, Jane "
    

总结

如果你喜欢本教程,记得点赞+收藏!关注我获取更多JavaScript/TypeScript开发干货

❌
❌