
最近折腾了 Wyrly DI,一个 TypeScript 的依赖注入(DI,Dependency Injection)工具库,顺手把踩坑经历整理一下。
Wyrly DI 面向现代 TypeScript 应用,主打的是不依赖 reflect-metadata、不依赖 emitDecoratorMetadata、不用老式装饰器、不用参数装饰器这几个特性。
核心特点:
说白了,"标准装饰器"指的是 TC39 提案的装饰器,TypeScript 5.0 之后默认支持那种。跟之前 experimentalDecorators 那套玩法不一样,很多现有的 DI 库都是基于老式装饰器写的。
很多 TypeScript DI 库依赖运行时元数据来推断构造函数依赖。
这样确实方便,但问题在于——重要的依赖信息被藏到了运行时元数据和装饰器行为后面,代码审查、静态分析、CI、AI 辅助工具都很难看到。
Wyrly DI 走了另一条路:显式声明。
import { Injectable, token } from "@wyrly/core"; type User = { id: string; name: string;
}; interface UserRepository { findById(id: string): Promise<User | null>;
} const UserRepositoryToken = token<UserRepository>("UserRepository"); @Injectable({ deps: [UserRepositoryToken], lifetime: "scoped",
})
class GetUserUseCase { constructor(private readonly users: UserRepository) {} execute(id: string) { return this.users.findById(id); }
}
通过 deps 声明依赖关系,代码审查、静态分析、CI、AI 辅助开发工具都能直接看到这些信息。
因为用的是标准装饰器,Wyrly DI 不会去读构造函数参数类型。构造函数照常写没问题(比如 constructor(private readonly users: UserRepository)),但依赖映射必须通过 deps 显式声明。
上面例子里用 UserRepositoryToken 是因为 TypeScript 接口在运行时不存在。如果依赖是类本身,类也可以直接当 Token 用。也就是说,接口用类型化 Token,类用类本身当 Token。
TypeScript 接口在运行时消失,所以基于接口的依赖需要运行时 Token。
Wyrly DI 提供了类型化的 Token 来解决这个问题。
const UserRepositoryToken = token<UserRepository>("UserRepository"); const users = scope.resolve(UserRepositoryToken);
// users: UserRepository
解析出来的值会被推导为 UserRepository 类型,所以基于接口的依赖依然能类型安全地使用。
Web 应用里,每个请求往往需要不同的依赖实例。
常见场景:
Wyrly DI 的 Web 适配器就是围绕这个模型设计的:
1 HTTP 请求 = 1 个 DI 作用域
1 GraphQL 请求 = 1 个 DI 作用域
请求作用域不是唯一支持的声明周期。核心包支持 singleton(单例)、scoped(作用域级)、transient(每次解析新建实例),可以覆盖应用级依赖、请求级依赖、需要每次创建新实例的依赖。
比如 Hono 适配器在中间件里创建每个请求的 Scope,然后在处理器里解析依赖。
import { Hono } from "hono";
import { createContainer } from "@wyrly/core";
import { di, getDI, type HonoDIVariables } from "@wyrly/hono"; const app = new Hono<{ Variables: HonoDIVariables }>();
const container = createContainer(); app.use(di(container)); app.get("/users/:id", async (c) => { const scope = getDI(c); const usecase = scope.resolve(GetUserUseCase); return c.json(await usecase.execute(c.req.param("id")));
});
Wyrly DI 支持检查和验证依赖图。
const graph = container.inspect(); const result = container.validate();
比如验证能检测出危险的声明周期组合:单例依赖作用域级依赖。
singleton -> scoped dependency
这种关系会埋下隐蔽的 bug,比如请求级的用户数据被单例持有,导致数据串场。
还可以在测试里加依赖图验证,CI 失败时直接报警。
import { assertEquals } from "@std/assert"; Deno.test("DI graph is valid", () => { const result = container.validate(); assertEquals( result.ok, true, result.issues.map((issue) => issue.message).join("\n"), );
});
小型应用可能觉得没必要。
但对于在意边界设计的项目,比如 DDD(领域驱动设计)或清洁架构风格,依赖问题发现得越晚,修复成本越高。
典型场景:
CurrentUser在 CI 里验证依赖图,能在这些问题变成"正常代码"之前抓住它们。
目前发布了这些包:
@wyrly/core
@wyrly/next
@wyrly/express
@wyrly/hono
@wyrly/fresh
@wyrly/graphql
Deno 用户用 JSR:
deno add jsr:@wyrly/core
Node.js 和 Bun 用户用 npm:
npm install @wyrly/core
按需安装适配器:
npm install @wyrly/hono
npm install @wyrly/next
npm install @wyrly/graphql
如果你的项目符合这些需求,Wyrly DI 可能是合适的选择:
reflect-metadata如果你的需求是这些,可能有别的工具更合适:
reflect-metadataWyrly DI 有意把一个轻量的显式 DI 核心和框架适配器分开设计。
仓库里包含了可运行的示例:
示例地址在 GitHub 上。
Wyrly DI 是一个让 TypeScript 依赖注入变得显式、可分析、适合 Web 请求作用域应用的工具库。
设计原则:
如果觉得对你的项目有用,从 @wyrly/core 开始,找最接近你使用框架的示例上手。
原文链接:https://dev.to/yoonesk/wyrly-di-type-safe-dependency-injection-for-modern-typescript-4f9i