Skip to content

TokenType 的使用场景

问题背景

在依赖注入中,最直接的写法是让一个类直接 import 并依赖另一个类:

ts
// OrderService.ts
import { PaymentService } from './PaymentService';
import { Inject, Injectable } from '@kaokei/di';

@Injectable()
class OrderService {
  @Inject(PaymentService)
  public paymentService!: PaymentService;
}

这样写的问题是 OrderService 强制依赖了 PaymentService 的具体实现。如果将来想替换支付服务的实现(比如从信用卡支付换成微信支付),就必须修改 OrderService 的代码。

解决方案:使用 Token + TokenType 解耦

核心思路是引入一个独立的 token.ts 文件,让服务之间通过 Token 间接依赖,而不是直接 import 对方。

文件结构

src/
├── token.ts            # 集中定义所有 Token
├── PaymentService.ts   # 支付服务(被依赖方)
├── OrderService.ts     # 订单服务(依赖方)
└── index.ts            # 组装容器,绑定 Token 和实现

第 1 步:定义 PaymentService(被依赖方)

PaymentService 是一个独立的服务,不需要 import 任何其他业务服务。

ts
// PaymentService.ts
import { Injectable } from '@kaokei/di';

@Injectable()
export class PaymentService {
  method = 'credit_card';

  pay(amount: number): string {
    const result = `通过 ${this.method} 支付了 ${amount} 元`;
    console.log(result);
    return result;
  }

  refund(orderId: string, amount: number): string {
    const result = `订单 ${orderId} 退款 ${amount} 元`;
    console.log(result);
    return result;
  }
}

第 2 步:定义 Token(解耦的关键)

token.ts 是集中管理所有 Token 的地方。这里使用 Token 创建标识符,使用 TokenType 导出对应的类型别名。

ts
// token.ts
import { Token, type TokenType } from '@kaokei/di';
import type { OrderService } from './OrderService';
import type { PaymentService } from './PaymentService';

// 订单服务的 Token
export const tokenOrderService = new Token<OrderService>('OrderService');
// TokenType<typeof tokenOrderService> 等价于 OrderService 类型
export type TokenOrderService = TokenType<typeof tokenOrderService>;

// 支付服务的 Token
export const tokenPaymentService = new Token<PaymentService>('PaymentService');
// TokenType<typeof tokenPaymentService> 等价于 PaymentService 类型
export type TokenPaymentService = TokenType<typeof tokenPaymentService>;

注意这里 import 的是 type,这意味着在运行时不会产生实际的 import,不会引入模块间的运行时依赖。

第 3 步:定义 OrderService(依赖方)

OrderService 依赖支付功能,但它不直接 import PaymentService,而是通过 Token 来声明依赖。

ts
// OrderService.ts
import { Inject, Injectable, type TokenType } from '@kaokei/di';
import { tokenPaymentService, type TokenPaymentService } from './token';

@Injectable()
export class OrderService {
  // 使用 Token 注入,而不是直接 @Inject(PaymentService)
  @Inject(tokenPaymentService)
  public paymentService!: TokenPaymentService;

  createOrder(orderId: string, amount: number): void {
    console.log(`创建订单:${orderId},金额:${amount} 元`);
    // 调用支付服务,这里有完整的类型提示
    this.paymentService.pay(amount);
    console.log(`订单 ${orderId} 创建成功`);
  }

  cancelOrder(orderId: string, amount: number): void {
    console.log(`取消订单:${orderId}`);
    this.paymentService.refund(orderId, amount);
    console.log(`订单 ${orderId} 已取消`);
  }
}

第 4 步:组装容器

index.ts 是唯一需要同时知道 Token 和具体实现类的地方。

ts
// index.ts
import { Container } from '@kaokei/di';
import { tokenOrderService, tokenPaymentService } from './token';
import { OrderService } from './OrderService';
import { PaymentService } from './PaymentService';

const container = new Container();

// 将 Token 绑定到具体的实现类
container.bind(tokenOrderService).to(OrderService);
container.bind(tokenPaymentService).to(PaymentService);

// 通过 Token 获取服务实例
const orderService = container.get(tokenOrderService);

orderService.createOrder('ORD-001', 299);
orderService.cancelOrder('ORD-001', 299);

TokenType 的作用

TokenType 是一个工具类型,用于从 Token 实例中提取泛型参数的类型。

ts
const tokenPaymentService = new Token<PaymentService>('PaymentService');

// 以下两种写法等价:
type A = TokenType<typeof tokenPaymentService>; // PaymentService
type B = PaymentService;                         // PaymentService

它的价值在于:你不需要直接 import PaymentService 类型,只需要 import tokenPaymentService 这个 Token,就能通过 TokenType 推导出正确的类型。这样在属性声明时既能获得完整的类型提示,又不会引入对具体实现的直接依赖。

对比总结

方面直接依赖Token 解耦
import 关系OrderService 直接 import PaymentServiceOrderService 只 import token.ts
注入写法@Inject(PaymentService)@Inject(tokenPaymentService)
类型声明paymentService!: PaymentServicepaymentService!: TokenPaymentService
替换实现需要修改 OrderService只需修改 index.ts 中的绑定
类型安全
运行时解耦

适用场景

  • 服务的实现可能会被替换(比如不同的支付方式、不同的数据库驱动)
  • 需要在测试中 mock 依赖服务
  • 多个模块之间需要降低耦合度
  • 希望在 index.ts(组装层)统一管理依赖关系