【进阶】AccessGuard v1.0:高级类型体操 --- TypeScript 类型安全的权限 DSL 深度实战
目录
- 前言
- 技术背景与演进逻辑
- 核心原理深度解析
- [TypeScript 类型系统作为计算语言](#TypeScript 类型系统作为计算语言)
- 类型级计算的四大基石
- 类型级"值"的表示方式
- 核心模块详解
- [模块一:HasPermission 类型级权限计算](#模块一:HasPermission 类型级权限计算)
- [模块二:AllOf / AnyOf 类型安全权限组合](#模块二:AllOf / AnyOf 类型安全权限组合)
- [模块三:品牌类型 --- 编译期 ID 防混淆](#模块三:品牌类型 — 编译期 ID 防混淆)
- 模块四:类型级角色层级检查
- [模块五:类型安全的权限 DSL](#模块五:类型安全的权限 DSL)
- [模块六:Type Challenges 权限专题](#模块六:Type Challenges 权限专题)
- [技术优缺点 & 适用场景](#技术优缺点 & 适用场景)
- 实战落地
- 全文总结
- 本期专栏更新说明
- 参考资料
前言
核心痛点 :在构建企业级权限系统时,权限检查逻辑的错误(如忘记 return early、ID 类型混用、角色层级误判)是生产环境中最隐蔽也最危险的安全漏洞。传统方案依赖运行时的 if 检查和代码审查,但人总会犯错。TypeScript 的类型系统是图灵完备的------我们可以让编译器在 IDE 中就发现权限配置错误,实现"编译不过即权限不对"的安全保障。
前置知识:需要掌握 TypeScript 泛型编程、条件类型(conditional types)、模板字面量类型、映射类型、递归类型、infer 关键字、索引访问类型。如果你已经跟随本系列前九篇文章逐步构建了 AccessGuard,本文的知识衔接将非常顺畅。
系列阶段:进阶篇第 6 篇(6/6)。本文是进阶篇的收官之作,标志着 AccessGuard 从 v0.9 升级到 v1.0,RBAC + ABAC 融合引擎的类型体系达到生产级完备状态。
收获能力:读完本文你将掌握:
- 类型级权限计算------用条件类型在编译期判断权限是否满足
- AllOf/AnyOf 类型安全组合------类型级 AND/OR 逻辑运算实现复杂权限约束
- 品牌类型(Branded Types)------在类型层面区分 RoleId/UserId/PermissionId 等语义不同但结构相同的 ID
- 类型级角色层级检查------用递归条件类型实现"父角色自动包含子角色权限"的编译期验证
- 类型安全的权限 DSL------设计一套编译期即可检查的权限领域特定语言
- Type Challenges 困难级类型体操能力
依赖版本(2026 年 6 月最新稳定版):
| 依赖 | 版本 | 说明 |
|---|---|---|
| TypeScript | 6.0 | 最新稳定版(TS 7.0 RC 已发布) |
| React | 19.1 | UI 框架 |
| Vite | 6.1 | 构建工具 |
| Zustand | 5.0 | 状态管理 |
| Vitest | 3.1 | 测试框架 |
技术背景与演进逻辑
从运行时到编译期:权限检查的五次范式跃迁
在 AccessGuard 系列的演进中,权限检查经历了从运行时到编译期的五次关键跃迁:
text
AccessGuard 权限检查演进路径
│
├── v0.1--v0.4:运行时 RBAC 检查
│ └── hasPermission(user, perm) → boolean
│ 核心:枚举 + 字面量类型,错误在运行时暴露
│
├── v0.5--v0.7:ABAC 属性模型 + 策略表达式 AST
│ └── Policy<T> 泛型约束,策略结构编译期验证
│ 核心:条件类型 infer,映射类型动态生成操作符
│
├── v0.8:RBAC + ABAC 融合引擎
│ └── AccessDecision = Allow | Deny | NotApplicable
│ 核心:类型收窄判断命中 RBAC 还是 ABAC
│
├── v0.9:策略可视化 + 模板字面量类型
│ └── 策略→自然语言描述的类型映射
│ 核心:递归类型遍历 AST,编译期策略语法检查
│
└── v1.0(本文):类型级权限计算 DSL
└── HasPermission<T, P> → true | false(字面量类型)
核心:类型级 AND/OR 逻辑、品牌类型 ID 防混淆、角色层级继承
为什么需要类型级权限计算?
传统运行时权限检查有三个致命缺陷:
缺陷一:忘记 return early
typescript
// ❌ 运行时 bug:忘记 return,继续执行了未授权操作
function deleteDocument(user: User, docId: string) {
if (!user.hasPermission('document:delete')) {
showError('无权限删除文档');
// 忘记 return!代码继续执行
}
api.deleteDocument(docId); // 安全漏洞!
}
缺陷二:ID 类型混用
typescript
// ❌ 运行时 bug:把 UserId 传给了需要 RoleId 的函数
function assignRole(roleId: string, userId: string) {
// roleId 和 userId 都是 string,编译器无法区分
// 实际调用时容易颠倒参数顺序
}
assignRole(userId, roleId); // 参数传反了,编译器不报错!
缺陷三:角色层级误判
typescript
// ❌ 运行时 bug:手动维护角色层级关系,容易遗漏
function checkAdminAccess(user: User): boolean {
// admin 应该继承 editor 和 viewer 的所有权限
// 但这里手动列出,容易遗漏更新
return user.role === 'admin' || user.role === 'editor';
}
类型级权限计算的解决方案:让 TypeScript 的类型系统在编译期就捕获这些错误。图灵完备的类型系统可以执行条件分支、递归、模式匹配------这正是实现权限 DSL 的理想"运行时"。
核心原理深度解析
TypeScript 类型系统作为计算语言
TypeScript 的类型系统是一个纯函数式、惰性求值的编程语言。理解它将类型视为"计算"而非"标注"是关键思维转变。
text
TypeScript 类型系统能力模型
│
├── 数据表示
│ ├── 基本类型:string, number, boolean, null, undefined, never, unknown
│ ├── 字面量类型:'admin', 42, true
│ ├── 复合类型:对象类型、元组类型、联合类型、交叉类型
│ └── 类型变量:泛型参数 <T>
│
├── 计算能力
│ ├── 条件分支:extends ? ... : ...(对应 if/else)
│ ├── 模式匹配:infer(对应解构赋值)
│ ├── 递归:类型引用自身(对应递归函数)
│ ├── 循环:映射类型遍历 key、递归类型遍历元组
│ └── 字符串操作:模板字面量类型(对应字符串插值+解析)
│
└── 类型体操特有的"标准库"
├── keyof:获取对象所有键的联合类型
├── T[K]:索引访问,获取属性类型
├── keyof any → string | number | symbol
└── never:空类型,用于过滤/终止递归
类型级计算的四大基石
基石一:条件类型 = 类型级 if/else
typescript
// 值级
const result = condition ? valueA : valueB;
// 类型级
type Result<T> = T extends Condition ? TypeA : TypeB;
条件类型的关键特性是分配律(Distributive Law):当 T 是联合类型时,条件类型自动分配到每个成员:
typescript
type IsString<T> = T extends string ? true : false;
type Test = IsString<string | number>; // true | false(分配的结果)
// 等价于 IsString<string> | IsString<number> → true | false
基石二:infer = 类型级模式匹配与解构
typescript
// 提取数组元素类型
type ElementOf<T> = T extends (infer E)[] ? E : never;
type E1 = ElementOf<string[]>; // string
// 提取 Promise 包裹的类型(递归)
type Awaited<T> = T extends Promise<infer U> ? Awaited<U> : T;
type A2 = Awaited<Promise<Promise<number>>>; // number
// 提取函数返回类型
type Return<T> = T extends (...args: any[]) => infer R ? R : never;
type R1 = Return<() => Permission[]>; // Permission[]
基石三:递归类型 = 类型级循环
typescript
// 递归遍历元组
type Includes<T extends readonly any[], U> =
T extends [infer First, ...infer Rest]
? Equal<First, U> extends true
? true
: Includes<Rest, U>
: false;
// 使用示例
type HasWrite = Includes<[ReadComment, WriteComment, DeletePost], WriteComment>;
// → true
基石四:模板字面量类型 = 类型级字符串解析
typescript
// 类型级字符串 split
type Split<S extends string, Sep extends string> =
S extends `${infer Head}${Sep}${infer Tail}`
? [Head, ...Split<Tail, Sep>]
: [S];
type Parts = Split<'admin:write:delete', ':'>;
// → ['admin', 'write', 'delete']
类型级"值"的表示方式
在类型级编程中,所有的"值"都是类型。理解"真值"和"假值"的类型表示至关重要:
| 值级概念 | 类型级表示 | 说明 |
|---|---|---|
true |
字面量类型 true |
类型级布尔真值 |
false |
字面量类型 false |
类型级布尔假值 |
boolean |
`true | false` |
数组 [a, b] |
元组类型 [A, B] |
类型级列表 |
空数组 [] |
[] |
类型级空列表,也用作终止条件 |
| 条件检查 | T extends U ? X : Y |
通过 assignability 检查 |
| 字符串匹配 | S extends TemplateLiteral |
模板字面量模式匹配 |
核心模块详解
模块一:HasPermission 类型级权限计算
这是 AccessGuard 1.0 最核心的类型工具------在编译期判断一个权限集合是否包含某个特定权限,返回字面量类型 true 或 false,而非宽泛的 boolean。
1.1 权限类型的建模前提
本系列从 v0.1 起就使用 const 断言(as const)和对象字面量定义权限,而非 enum。这个选择对于类型级运算至关重要:
typescript
// ✅ 对象 + as const --- 保留字面量类型信息,支持 union 和 intersection
const ReadComment = { readComment: true } as const;
const WriteComment = { writeComment: true } as const;
const DeletePost = { deletePost: true } as const;
type ReadComment = typeof ReadComment;
type WriteComment = typeof WriteComment;
type DeletePost = typeof DeletePost;
// 联合类型:或关系
type ModPermissions = WriteComment | DeletePost;
// 交叉类型:且关系
type FullAccess = ReadComment & WriteComment & DeletePost;
// ❌ enum --- 不支持 intersection,无法表达"同时拥有多个权限"
// enum Permission { ReadComment, WriteComment }
// type Both = Permission.ReadComment & Permission.WriteComment; // → never!
1.2 基础 HasPermission 实现
typescript
/**
* 类型级权限检查
* HasPermission<UserPermissions, RequiredPermission>
* 返回 true(字面量)或 false(字面量),不是 boolean
*
* 原理:利用联合类型的分配律------如果 RequiredPermission 是 UserPermissions
* 联合类型的成员之一,extends 判定即为 true
*/
type HasPermission<
UserPermissions extends Permission,
RequiredPermission extends Permission
> = RequiredPermission extends UserPermissions ? true : false;
// ── 测试用例 ──
type UserPerms = ReadComment | WriteComment;
// 用户确实有 WriteComment → true
type CanWrite = HasPermission<UserPerms, WriteComment>;
// ^? type CanWrite = true
// 用户没有 DeletePost → false
type CanDelete = HasPermission<UserPerms, DeletePost>;
// ^? type CanDelete = false
// 用户有 ReadComment 或 WriteComment 中的任意一个 → true
type CanReadOrWrite = HasPermission<UserPerms, ReadComment | WriteComment>;
// ^? type CanReadOrWrite = true(因为联合分配后每一项都 extends)
// 用户同时需要 ReadComment 和 DeletePost → false
// (因为 DeletePost 不是 UserPerms 的成员,分配后该项为 false)
type NeedsBoth = HasPermission<UserPerms, ReadComment & DeletePost>;
// ^? type NeedsBoth = false
关键理解 :HasPermission<UserPerms, ReadComment & DeletePost> 返回 false 是因为交叉类型 ReadComment & DeletePost 作为一个整体去 extends 判断,而不是分别判断。这引出了下一个需求------我们需要 AllOf 来处理"同时满足多个权限"的场景。
1.3 深度增强:区分"未检查"与"拒绝"
在生产系统中,我们需要三个状态而非两个:
typescript
/**
* 权限检查结果的三态类型
*/
type AccessResult = 'allow' | 'deny' | 'not_applicable';
/**
* 增强版 HasPermission ------ 返回三态而非二态
*/
type CheckPermission<
UserPermissions extends Permission,
RequiredPermission extends Permission
> = [RequiredPermission] extends [never]
? 'not_applicable'
: RequiredPermission extends UserPermissions
? 'allow'
: 'deny';
// ── 测试 ──
type T1 = CheckPermission<ReadComment | WriteComment, WriteComment>; // 'allow'
type T2 = CheckPermission<ReadComment, DeletePost>; // 'deny'
type T3 = CheckPermission<ReadComment, never>; // 'not_applicable'
模块二:AllOf / AnyOf 类型安全权限组合
这是类型级 AND/OR 逻辑运算的核心实现,利用递归条件类型和可变元组类型(Variadic Tuple Types)实现任意数量的权限组合检查。
2.1 AllOf --- 要求满足全部权限
typescript
/**
* AllOf<UserPermissions, [Perm1, Perm2, ...]>
* 检查用户是否同时拥有列表中所有权限
* 原理:递归遍历权限元组,遇到第一个不满足的返回 false
* 全部满足返回 true
*/
type AllOf<
UserPermissions extends Permission,
RequiredList extends Permission[]
> = RequiredList extends [infer First, ...infer Rest]
? First extends Permission
? HasPermission<UserPermissions, First> extends true
? Rest extends Permission[]
? AllOf<UserPermissions, Rest>
: true
: false
: never
: true; // 空列表 → 全部满足
// ── 测试用例 ──
type AdminPerms = ReadComment & WriteComment & DeletePost & BanUser;
// 全部满足 → true
type AdminCanManage = AllOf<AdminPerms, [ReadComment, WriteComment, DeletePost]>;
// ^? type AdminCanManage = true
// 缺少 BanUser → false
type ViewerAllOf = AllOf<ReadComment, [ReadComment, WriteComment]>;
// ^? type ViewerAllOf = false
// 空列表 → true(vacuously true)
type EmptyAllOf = AllOf<AdminPerms, []>;
// ^? type EmptyAllOf = true
2.2 AnyOf --- 要求满足任意一个权限
typescript
/**
* AnyOf<UserPermissions, [Perm1, Perm2, ...]>
* 检查用户是否至少拥有列表中一个权限
* 原理:递归遍历权限元组,遇到第一个满足的立即返回 true
* 全部不满足返回 false
*/
type AnyOf<
UserPermissions extends Permission,
RequiredList extends Permission[]
> = RequiredList extends [infer First, ...infer Rest]
? First extends Permission
? HasPermission<UserPermissions, First> extends true
? true
: Rest extends Permission[]
? AnyOf<UserPermissions, Rest>
: false
: false
: false; // 空列表 → 无任何权限满足
// ── 测试用例 ──
type ViewerPerms = ReadComment;
// 至少有一个 ReadComment → true
type ViewerCanView = AnyOf<ViewerPerms, [ReadComment, WriteComment]>;
// ^? type ViewerCanView = true
// 一个都没有 → false
type ViewerCannotMod = AnyOf<ViewerPerms, [WriteComment, DeletePost]>;
// ^? type ViewerCannotMod = false
2.3 组合使用:复杂业务规则的类型编码
typescript
/**
* 实际业务场景:文档管理权限
* - 查看文档:需要 ReadDocument
* - 编辑草稿:需要 ReadDocument AND WriteDocument
* - 管理员操作:需要 ReadDocument AND WriteDocument AND DeleteDocument
* - 内容审核:需要 ReadDocument OR ReportContent
*/
// 文档查看权限
type CanViewDocument<T extends Permission> = HasPermission<T, ReadDocument>;
// 文档编辑权限
type CanEditDocument<T extends Permission> = AllOf<
T, [ReadDocument, WriteDocument]
>;
// 管理员权限
type CanAdminDocument<T extends Permission> = AllOf<
T, [ReadDocument, WriteDocument, DeleteDocument]
>;
// 审核权限(联合判断)
type CanModerate<T extends Permission> = AnyOf<
T, [ReadDocument, ReportContent]
>;
// ── 编译期权限门控函数 ──
declare function viewDocument<T extends Permission>(
perms: T & (CanViewDocument<T> extends true ? {} : never),
docId: string
): void;
declare function editDocument<T extends Permission>(
perms: T & (CanEditDocument<T> extends true ? {} : never),
docId: string
): void;
模块三:品牌类型 --- 编译期 ID 防混淆
3.1 问题分析
在企业级 IAM 系统中,存在多种语义不同但结构相同的 ID 类型:
typescript
// ❌ 结构类型系统的陷阱:以下三个 ID 类型结构上完全相同
type RoleId = string;
type UserId = string;
type PermissionId = string;
// 因为都是 string,TypeScript 允许任意混用!
function assignRole(roleId: RoleId, userId: UserId): void { /* ... */ }
const uid: UserId = 'user-001';
const rid: RoleId = 'role-admin';
assignRole(uid, rid); // 参数传反了,类型检查完全通过!
TypeScript 使用结构化类型系统(Structural Typing),两个结构相同的类型被视为兼容。品牌类型(Branded Types,也称 Opaque Types 或 Nominal Types)通过在类型上附加一个唯一的"品牌"标记来打破结构等价性。
3.2 品牌类型基础实现
typescript
/**
* 品牌类型核心工具
* 使用 unique symbol 创建编译期唯一的"幻影属性"
*/
// 品牌声明(仅在类型层面存在,运行时无任何开销)
declare const RoleIdBrand: unique symbol;
declare const UserIdBrand: unique symbol;
declare const PermissionIdBrand: unique symbol;
declare const TenantIdBrand: unique symbol;
// 品牌类型定义
type RoleId = string & { [RoleIdBrand]: true };
type UserId = string & { [UserIdBrand]: true };
type PermissionId = string & { [PermissionIdBrand]: true };
type TenantId = string & { [TenantIdBrand]: true };
// ── 类型安全的函数签名 ──
declare function assignRole(roleId: RoleId, userId: UserId): void;
declare function grantPermission(permId: PermissionId, roleId: RoleId): void;
declare function addUserToTenant(userId: UserId, tenantId: TenantId): void;
// ── 编译期错误演示 ──
declare const uid: UserId;
declare const rid: RoleId;
declare const pid: PermissionId;
assignRole(rid, uid); // ✅ 参数顺序正确
assignRole(uid, rid); // ❌ 类型错误!UserId 不能赋给 RoleId
grantPermission(uid, rid); // ❌ 类型错误!
3.3 品牌类型的工厂模式
手工为每个 ID 做品牌断言既繁琐又容易出错。我们可以创建类型安全的工厂函数:
typescript
/**
* 品牌 ID 工厂
* 每个工厂函数在运行时只是透传字符串,
* 但在编译期为值打上类型品牌标记
*/
// ── 核心工厂类型 ──
type Brand<TBase, TBrand extends string> = TBase & { __brand: TBrand };
// ── ID 品牌定义 ──
type RoleId = Brand<string, 'RoleId'>;
type UserId = Brand<string, 'UserId'>;
type PermissionId = Brand<string, 'PermissionId'>;
// ── 工厂函数(使用 as 断言在编译期打上品牌) ──
const RoleId = (value: string): RoleId => value as RoleId;
const UserId = (value: string): UserId => value as UserId;
const PermissionId = (value: string): PermissionId => value as PermissionId;
// ── 运行时零开销,编译期完全隔离 ──
const adminRole = RoleId('role-admin-001');
const userJohn = UserId('user-john-001');
const writePerm = PermissionId('perm-write-001');
// 每个 ID 的类型完全隔离
assignRole(adminRole, userJohn); // ✅
// assignRole(userJohn, adminRole); // ❌ 编译错误
3.4 品牌类型的 UUID 变体
为不同类型的数据实体创建不同的品牌 ID:
typescript
/**
* 基于品牌类型的实体 ID 系统
*/
type EntityId<T extends string> = Brand<string, T>;
type DocumentId = EntityId<'Document'>;
type ProjectId = EntityId<'Project'>;
type CommentId = EntityId<'Comment'>;
// 利用辅助函数创建品牌 ID
const DocumentId = (value: string): DocumentId => value as DocumentId;
const ProjectId = (value: string): ProjectId => value as ProjectId;
const CommentId = (value: string): CommentId => value as CommentId;
// ── 类型安全的 API ──
declare function fetchDocument(id: DocumentId): Promise<Document>;
declare function fetchProject(id: ProjectId): Promise<Project>;
const docId = DocumentId('doc-001');
const projId = ProjectId('proj-001');
fetchDocument(docId); // ✅
// fetchDocument(projId); // ❌ 类型错误:ProjectId 不能赋给 DocumentId
品牌类型运行时开销 :零。Brand<TBase, TBrand> 中的 { __brand: TBrand } 仅在类型层面存在,编译后的 JavaScript 中没有任何额外属性或代码。
模块四:类型级角色层级检查
4.1 角色层级问题
在企业 IAM 中,角色之间存在继承关系:
text
AccessGuard 角色层级树
│
├── super_admin(超级管理员)
│ ├── 继承:admin 的全部权限
│ ├── 独有:系统配置、租户管理
│ └── 独有:审计日志查看
│
├── admin(管理员)
│ ├── 继承:editor 的全部权限
│ ├── 独有:用户管理、角色分配
│ └── 独有:权限策略配置
│
├── editor(编辑者)
│ ├── 继承:viewer 的全部权限
│ ├── 独有:创建/编辑内容
│ └── 独有:发布内容
│
└── viewer(查看者)
└── 独有:查看内容
我们需要类型系统理解 super_admin 自动拥有 admin 的所有权限,以及 admin 自动拥有 editor 的所有权限。
4.2 角色层级声明与类型级继承检查
typescript
/**
* 角色层级映射表(值级)
* 用 const 断言保留字面量类型信息
*/
const RoleHierarchy = {
super_admin: ['admin', 'editor', 'viewer'],
admin: ['editor', 'viewer'],
editor: ['viewer'],
viewer: [],
} as const;
type RoleHierarchy = typeof RoleHierarchy;
type RoleName = keyof RoleHierarchy;
/**
* 获取子角色列表(类型级)
*/
type ChildRoles<R extends RoleName> = RoleHierarchy[R][number];
// ── 测试 ──
type AdminChildren = ChildRoles<'admin'>;
// ^? type AdminChildren = 'editor' | 'viewer'
/**
* 类型级角色继承检查
* InheritsRole<Role, Target>
* 判断 Role 是否继承(直接或间接)Target
*/
type InheritsRole<
Role extends RoleName,
Target extends RoleName
> = Role extends Target
? true // 自身
: Target extends ChildRoles<Role>
? true // 直接子角色
: {
[K in ChildRoles<Role>]: InheritsRole<K, Target>
}[ChildRoles<Role>]; // 递归检查间接子角色
// ── 测试用例 ──
type T1 = InheritsRole<'super_admin', 'viewer'>; // true
type T2 = InheritsRole<'admin', 'viewer'>; // true
type T3 = InheritsRole<'viewer', 'admin'>; // false(下级不能继承上级)
type T4 = InheritsRole<'editor', 'editor'>; // true(自身)
上述递归类型通过映射类型的 union distribution 特性实现递归分发:{ [K in ChildRoles<Role>]: InheritsRole<K, Target> }[ChildRoles<Role>] 这个模式会对每个子角色独立求值,返回所有结果的联合类型------如果任意一个为 true,结果即 true。
4.3 结合品牌类型的完整角色-权限模型
typescript
/**
* AccessGuard 1.0 完整角色-权限类型模型
*/
// ── 品牌 ID ──
type RoleId = Brand<string, 'RoleId'>;
type PermissionId = Brand<string, 'PermissionId'>;
type UserId = Brand<string, 'UserId'>;
// ── 角色定义 ──
interface Role {
id: RoleId;
name: RoleName;
permissions: Permission[];
}
// ── 用户定义 ──
interface User {
id: UserId;
roles: RoleId[];
}
// ── 类型级权限聚合:从用户角色列表计算全量权限 ──
type GetRolePermissions<R extends RoleName> = {
[K in keyof RolePermissionsMap]: K extends R ? RolePermissionsMap[K] : never
}[keyof RolePermissionsMap];
// ── 用户全量权限计算 ──
type UserPermissions<U extends User> = U extends { roles: infer R }
? R extends RoleId[]
? // 简化版:通过 RoleName 查找权限
never // 实际实现略复杂,此处展示类型签名
: never
: never;
模块五:类型安全的权限 DSL
将前述所有模块整合起来,构建一套类型安全的权限领域特定语言(DSL),让权限策略配置在 IDE 中就能实时获得类型反馈。
5.1 DSL 语法设计
text
AccessGuard 1.0 权限 DSL 语法
│
├── 原子权限
│ ├── 'document:read' → type DocRead = typeof DocumentRead
│ ├── 'document:write' → type DocWrite = typeof DocumentWrite
│ └── 'document:delete' → type DocDelete = typeof DocumentDelete
│
├── 权限组合(类型级逻辑运算)
│ ├── AllOf<[A, B, C]> → 需要全部满足(AND)
│ ├── AnyOf<[A, B, C]> → 需要任一满足(OR)
│ └── Not<A> → 排除某个权限(通过 Exclude 实现)
│
├── 角色层级(类型级继承)
│ ├── InheritsRole<'admin', 'viewer'> → 自动传递
│ └── 角色标签:'role:admin' → 展开为子角色权限全集
│
└── 策略表达式(编译期可计算)
├── Policy<{ allow: AllOf<[DocRead, DocWrite]>, deny: never }>
└── 策略评估结果:AccessResult('allow' | 'deny' | 'not_applicable')
5.2 DSL 核心类型实现
typescript
/**
* 权限 DSL 核心类型定义
*/
// ── 权限匹配器 ──
type PermissionMatcher<T extends Permission> = {
// 精确匹配
exact: <P extends T>(perm: P) => P;
// AllOf 组合
allOf: <P extends Permission[]>(...perms: P) => AllOf<T, P>;
// AnyOf 组合
anyOf: <P extends Permission[]>(...perms: P) => AnyOf<T, P>;
};
// ── 策略规则 ──
interface PolicyRule {
subject?: {
roles?: RoleName[];
userIds?: UserId[];
};
resource?: {
type?: string;
ownerId?: UserId;
};
action: string;
condition?: string; // ABAC 条件表达式
}
// ── 类型级策略匹配 ──
type MatchPolicy<
TPermissions extends Permission,
TRoles extends RoleName[],
TPolicy extends PolicyRule
> = (
// 检查角色要求(AnyOf)
TPolicy['subject'] extends { roles: infer R }
? R extends RoleName[]
? AnyOf<TRoles, R>
: true
: true
) extends true
? (
// 检查权限要求(AllOf)
HasPermission<TPermissions, InferPermissionFromAction<TPolicy['action']>>
)
: false;
// ── 辅助:从 action 字符串推断所需权限类型 ──
type InferPermissionFromAction<A extends string> =
A extends `document:read` ? typeof ReadDocument :
A extends `document:write` ? typeof WriteDocument :
A extends `document:delete` ? typeof DeleteDocument :
A extends `user:manage` ? typeof ManageUsers :
never;
5.3 使用 DSL 定义策略
typescript
/**
* 策略定义------编译期即可验证正确性
*/
// ── 文档管理策略 ──
const DocumentPolicies = {
// 查看文档:只需读权限
view: {
subject: { roles: ['viewer', 'editor', 'admin', 'super_admin'] as const },
action: 'document:read' as const,
},
// 编辑文档:需要写权限 + editor 以上角色
edit: {
subject: { roles: ['editor', 'admin', 'super_admin'] as const },
action: 'document:write' as const,
},
// 删除文档:需要删除权限 + admin 以上角色
delete: {
subject: { roles: ['admin', 'super_admin'] as const },
action: 'document:delete' as const,
},
} as const;
// ── 类型安全的策略评估函数 ──
function evaluatePolicy<
P extends Permission,
R extends readonly RoleName[]
>(
userPerms: P,
userRoles: R,
policy: PolicyRule
): boolean {
// 运行时实现 + 编译期类型约束
// 如果策略要求 userRoles 中有 'admin',
// 但传入的 userRoles 类型中没有 'admin',编译器会报错
return true; // 简化示例
}
模块六:Type Challenges 权限专题
以下是从 TypeScript 类型挑战社区精选的五个困难级(Hard)权限相关题目,覆盖了类型级权限计算的核心难题。
Challenge 1:ExcludePermission
从权限联合类型中排除某个权限:
typescript
/**
* 挑战:实现 ExcludePermission<T, P>
* 从权限联合类型 T 中排除权限 P
*/
type ExcludePermission<T, P> = T extends P ? never : T;
// ── 测试 ──
type AllPerms = ReadDocument | WriteDocument | DeleteDocument;
type WithoutWrite = ExcludePermission<AllPerms, WriteDocument>;
// ^? type WithoutWrite = ReadDocument | DeleteDocument
Challenge 2:PermissionIntersection
计算两个用户共有的权限:
typescript
/**
* 挑战:实现 PermissionIntersection<A, B>
* 返回两个权限集合的交集
*/
type PermissionIntersection<A extends Permission, B extends Permission> =
Extract<A, B>;
// ── 测试 ──
type Alice = ReadDocument | WriteDocument;
type Bob = WriteDocument | DeleteDocument;
type Shared = PermissionIntersection<Alice, Bob>;
// ^? type Shared = WriteDocument
Challenge 3:DeepRequiredPermission
递归地将所有权限标记为必需:
typescript
/**
* 挑战:实现 DeepRequiredPermission<T>
* 递归地将嵌套权限对象的所有属性变为 required
*/
type DeepRequiredPermission<T> = {
[K in keyof T]-?: T[K] extends object
? DeepRequiredPermission<T[K]>
: T[K];
};
// ── 测试 ──
interface PermissionConfig {
document?: {
read?: boolean;
write?: boolean;
};
user?: {
manage?: boolean;
};
}
type StrictConfig = DeepRequiredPermission<PermissionConfig>;
// 所有属性变为 required
Challenge 4:RolePermissionExpand
将角色标签展开为对应的权限集合:
typescript
/**
* 挑战:实现 RolePermissionExpand<R>
* 根据角色层级,将角色类型展开为完整权限集合
*/
type RoleToPermissions<R extends RoleName> = {
super_admin: ReadDocument | WriteDocument | DeleteDocument | ManageUsers | ManageRoles;
admin: ReadDocument | WriteDocument | DeleteDocument | ManageUsers;
editor: ReadDocument | WriteDocument;
viewer: ReadDocument;
}[R];
type AdminPermissions = RoleToPermissions<'admin'>;
// ^? ReadDocument | WriteDocument | DeleteDocument | ManageUsers
Challenge 5:PermissionPath --- 模板字面量递归类型
typescript
/**
* 挑战:实现 PermissionPath<T, Prefix>
* 给定嵌套权限对象,生成所有权限路径的模板字面量联合类型
*
* 示例输入:
* { document: { read: true; write: true }; user: { manage: true } }
*
* 期望输出:
* 'document:read' | 'document:write' | 'user:manage'
*/
type PermissionPath<
T,
Prefix extends string = ''
> = {
[K in keyof T & string]: T[K] extends object
? PermissionPath<T[K], Prefix extends '' ? K : `${Prefix}:${K}`>
: Prefix extends '' ? K : `${Prefix}:${K}`;
}[keyof T & string];
// ── 测试 ──
const PermTree = {
document: { read: true, write: true, delete: true },
user: { manage: true, invite: true },
} as const;
type PermTree = typeof PermTree;
type AllPermPaths = PermissionPath<PermTree>;
// ^? 'document:read' | 'document:write' | 'document:delete'
// | 'user:manage' | 'user:invite'
技术优缺点 & 适用场景
技术优势
| 优势 | 详细说明 |
|---|---|
| 编译期安全 | 权限配置错误在 IDE 中即时暴露,不等运行时才发现 |
| 零运行时开销 | 所有类型级计算在编译后完全擦除,不影响 bundle 大小 |
| 自文档化 | 类型签名即文档,API 的权限要求一目了然 |
| 重构安全 | 修改权限枚举后,所有不兼容的调用点立即收到类型错误 |
| 渐进式采用 | 可以在现有项目中逐步引入类型级约束,不需要一次性重写 |
| ID 防混淆 | 品牌类型杜绝了 UserId/RoleId/PermissionId 的意外混用 |
现存局限
| 局限 | 详细说明 |
|---|---|
| 学习曲线陡峭 | 递归条件类型、模板字面量类型等概念需要时间消化 |
| 类型错误信息复杂 | 深层递归类型的错误信息可能难以解读 |
| 编译性能 | 大量递归类型计算会延长 tsc --noEmit 时间 |
| 泛型函数传参 | 类型级计算主要在泛型函数签名中生效,普通变量赋值仍需要类型断言 |
| TS 版本限制 | 可变元组类型需要 TS 4.0+,模板字面量类型需要 TS 4.1+,品牌类型依赖 unique symbol |
生产适用场景
- 企业级权限管理系统:多角色、多资源类型、细粒度权限控制,权限矩阵复杂度超过 100 种权限
- SaaS 多租户平台:租户隔离 + 租户内角色管理,需要类型层面区分不同租户的 ID 空间
- 合规敏感应用:金融、医疗、政务等需要审计追溯的领域,权限检查必须有编译期强保证
禁忌场景
- 快速原型开发:权限需求尚未固化的早期阶段,过度类型化会拖慢迭代速度
- 权限逻辑极其简单的应用:只有 admin/user 两种角色的系统,类型体操收益不足以覆盖成本
- 非 TypeScript 后端项目:类型级计算仅限于前端编译期,后端仍需独立的权限验证(前后端权限校验是互为补充的关系,前端类型验证不可替代后端校验)
实战落地
完整可运行项目搭建
Step 1:项目初始化
bash
# 使用 Vite 创建 React + TypeScript 项目
npm create vite@latest accessguard-v1 -- --template react-ts
cd accessguard-v1
# 安装依赖
npm install zustand@5 react@19 react-dom@19
npm install -D vitest@3 @testing-library/react typescript@6
# tsconfig.json 核心配置(TypeScript 6.0)
json
{
"compilerOptions": {
"strict": true,
"module": "esnext",
"moduleResolution": "bundler",
"target": "es2025",
"lib": ["es2025", "dom"],
"types": ["node"],
"jsx": "react-jsx",
"noUncheckedIndexedAccess": true,
"exactOptionalPropertyTypes": true,
"noUnusedLocals": true,
"noUnusedParameters": true
}
}
Step 2:核心类型定义
typescript
// src/types/brand.ts --- 品牌类型工具
export type Brand<TBase, TBrand extends string> = TBase & { __brand: TBrand };
export type RoleId = Brand<string, 'RoleId'>;
export type UserId = Brand<string, 'UserId'>;
export type PermissionId = Brand<string, 'PermissionId'>;
export type TenantId = Brand<string, 'TenantId'>;
export const RoleId = (v: string) => v as RoleId;
export const UserId = (v: string) => v as UserId;
export const PermissionId = (v: string) => v as PermissionId;
// src/types/permissions.ts --- 权限定义与类型级运算
export const ReadDocument = { readDocument: true } as const;
export const WriteDocument = { writeDocument: true } as const;
export const DeleteDocument = { deleteDocument: true } as const;
export const ManageUsers = { manageUsers: true } as const;
export const ManageRoles = { manageRoles: true } as const;
export type ReadDocument = typeof ReadDocument;
export type WriteDocument = typeof WriteDocument;
export type DeleteDocument = typeof DeleteDocument;
export type ManageUsers = typeof ManageUsers;
export type ManageRoles = typeof ManageRoles;
export type Permission =
| ReadDocument
| WriteDocument
| DeleteDocument
| ManageUsers
| ManageRoles;
// 类型级 HasPermission
export type HasPermission<
TPerms extends Permission,
TRequired extends Permission
> = TRequired extends TPerms ? true : false;
// 类型级 AllOf
export type AllOf<
TPerms extends Permission,
TList extends Permission[]
> = TList extends [infer First, ...infer Rest]
? First extends Permission
? HasPermission<TPerms, First> extends true
? Rest extends Permission[]
? AllOf<TPerms, Rest>
: true
: false
: never
: true;
// 类型级 AnyOf
export type AnyOf<
TPerms extends Permission,
TList extends Permission[]
> = TList extends [infer First, ...infer Rest]
? First extends Permission
? HasPermission<TPerms, First> extends true
? true
: Rest extends Permission[]
? AnyOf<TPerms, Rest>
: false
: false
: false;
Step 3:React Hook 集成
typescript
// src/hooks/usePermission.ts
import { useMemo } from 'react';
import type { Permission, HasPermission, AllOf, AnyOf } from '../types/permissions';
/**
* 类型安全的权限检查 Hook
* 返回具有字面量类型级别的权限检查函数
*/
export function usePermission<TPerms extends Permission>(userPermissions: TPerms) {
return useMemo(() => ({
// 运行时检查 + 编译期类型约束
has<P extends Permission>(perm: P): boolean {
// 实际实现对权限集合进行检查
return true; // 简化
},
// 类型级信息
_type: null as unknown as {
hasPermission: HasPermission<TPerms, Permission>,
permissions: TPerms,
}
}), [userPermissions]);
}
// src/components/Can.tsx
import React from 'react';
import type { Permission, HasPermission } from '../types/permissions';
interface CanProps<
TPerms extends Permission,
TRequired extends Permission
> {
userPermissions: TPerms;
required: TRequired;
/** 类型级验证:确保 Required 是 UserPermissions 的子集 */
_typeCheck: HasPermission<TPerms, TRequired>;
children: React.ReactNode;
fallback?: React.ReactNode;
}
export function Can<
TPerms extends Permission,
TRequired extends Permission
>({ children, fallback = null }: CanProps<TPerms, TRequired>) {
// 运行时检查
return <>{children}</>;
}
Step 4:Zustand Store 集成
typescript
// src/store/authStore.ts
import { create } from 'zustand';
import type { UserId, RoleId } from '../types/brand';
import type { Permission } from '../types/permissions';
interface AuthState {
userId: UserId | null;
roleIds: RoleId[];
permissions: Permission[];
login: (uid: UserId, roles: RoleId[], perms: Permission[]) => void;
logout: () => void;
}
export const useAuthStore = create<AuthState>((set) => ({
userId: null,
roleIds: [],
permissions: [],
login: (userId, roleIds, permissions) =>
set({ userId, roleIds, permissions }),
logout: () => set({ userId: null, roleIds: [], permissions: [] }),
}));
Step 5:类型测试
typescript
// src/__tests__/type-level.test.ts
import { describe, it, expectTypeOf } from 'vitest';
import type {
ReadDocument, WriteDocument, DeleteDocument,
HasPermission, AllOf, AnyOf, Permission
} from '../types/permissions';
describe('AccessGuard 1.0 Type-Level Tests', () => {
it('HasPermission 返回字面量类型', () => {
type UserPerms = ReadDocument | WriteDocument;
// 类型级断言(使用 expect-type 风格)
type CanRead = HasPermission<UserPerms, ReadDocument>;
expectTypeOf<CanRead>().toEqualTypeOf<true>();
type CanDelete = HasPermission<UserPerms, DeleteDocument>;
expectTypeOf<CanDelete>().toEqualTypeOf<false>();
});
it('AllOf 正确执行 AND 逻辑', () => {
type AdminPerms = ReadDocument | WriteDocument | DeleteDocument;
type AllPresent = AllOf<AdminPerms, [ReadDocument, WriteDocument]>;
expectTypeOf<AllPresent>().toEqualTypeOf<true>();
type Missing = AllOf<ReadDocument, [ReadDocument, WriteDocument]>;
// Missing 为 false(因为缺少 WriteDocument)
});
it('AnyOf 正确执行 OR 逻辑', () => {
type ReadOnly = ReadDocument;
type HasOne = AnyOf<ReadOnly, [ReadDocument, WriteDocument]>;
expectTypeOf<HasOne>().toEqualTypeOf<true>();
});
});
AccessGuard 1.0 类型体系回顾
AccessGuard 从 v0.1 到 v1.0 共 10 篇文章,构建了完整的前端类型安全权限系统。以下是全体系回顾:
text
AccessGuard 1.0 类型体系全景图
│
├── 核心类型层(v0.1--v0.4)
│ ├── 权限定义:as const 对象字面量 → 字面量类型
│ ├── 角色类型:interface Role { permissions: Permission[] }
│ ├── 用户类型:interface User { roles: RoleId[] }
│ └── 类型守卫:hasPermission(user, perm) → user is AuthorizedUser<T>
│
├── 策略引擎层(v0.5--v0.8)
│ ├── ABAC 属性模型:Discriminated Union 建模 Subject/Resource/Action/Env
│ ├── 策略表达式 AST:Eq/Neq/Contains/And/Or/Not 类型安全 AST
│ ├── 映射类型编辑器:根据属性类型动态生成操作符
│ └── RBAC + ABAC 融合:交叉类型 + 类型收窄 → AccessDecision
│
├── 可视化与 DSL 层(v0.9--v1.0)
│ ├── 模板字面量类型:策略 → 自然语言描述
│ ├── 递归类型:遍历策略 AST,编译期语法检查
│ ├── 类型级 HasPermission:Compile-time boolean literal
│ ├── AllOf / AnyOf:类型级逻辑组合
│ ├── 品牌类型:ID 防混淆,编译期强制区分语义
│ └── 角色层级继承:类型级递归角色传递检查
│
└── 运行时集成(全系列贯穿)
├── React 泛型组件:<Can permission={...}> 守卫组件
├── Zustand Store:类型安全权限状态管理
├── Vitest 类型测试:expectTypeOf 类型断言
└── Vite 构建:ESM + TypeScript 6.0
生产避坑经验
坑一:递归类型深度过大导致类型计算超时
typescript
// ❌ 无限递归的风险------忘记终止条件
type BadRecurse<T> = T extends infer U ? BadRecurse<U> : T;
// ✅ 始终设定明确的终止条件
type GoodRecurse<T, Depth extends number = 0> =
Depth extends 5 ? T : // 最大深度限制
T extends SomePattern ? GoodRecurse<Next, Increment<Depth>> : T;
TypeScript 的类型递归深度默认限制为 50 层。对于权限系统,通常 10 层以内就足够。如果接近限制,考虑使用尾递归优化或分步计算。
坑二:条件类型分配律导致意外结果
typescript
// ⚠️ 分配律陷阱
type IsString<T> = T extends string ? true : false;
type TestUnion = IsString<'a' | 1>; // true | false(分配的结果)
// ✅ 用元组包裹禁用分配律
type IsStringNoDistribute<T> = [T] extends [string] ? true : false;
type TestNoDist = IsStringNoDistribute<'a' | 1>; // false(整体判断)
坑三:品牌类型不能直接在 JSON 序列化中使用
typescript
// ❌ 不能 JSON.parse 后直接断言为品牌类型
const raw = JSON.parse('"user-001"') as UserId; // 绕过了类型检查!
// ✅ 使用验证函数包装
function parseUserId(raw: unknown): UserId | Error {
if (typeof raw === 'string' && raw.startsWith('user-')) {
return raw as UserId;
}
return new Error(`Invalid UserId: ${raw}`);
}
坑四:type vs interface 在品牌类型中的差异
typescript
// ✅ type 声明------品牌信息保留
type RoleId = string & { __brand: 'RoleId' };
// ❌ interface 声明------extends 后的交叉类型信息在 IDE 提示中可能丢失
// interface RoleId extends string { __brand: 'RoleId' } // 不推荐
全文总结
本文作为 AccessGuard 进阶篇的收官之作,将 TypeScript 类型系统的图灵完备能力发挥到极致,实现了编译期的权限计算 DSL。
核心原理回顾:
- TypeScript 类型系统是纯函数式编程语言,条件类型 = if/else,infer = 模式匹配,递归类型 = 循环,模板字面量类型 = 字符串解析
- 类型级"值"用字面量类型表示:
true/false而非boolean
关键实现:
- HasPermission<T, P> :利用联合类型分配律,返回字面量
true | false - AllOf/AnyOf:递归遍历权限元组,实现 AND/OR 逻辑运算
- 品牌类型 :
string & { [unique symbol]: true }模式,零运行时开销的 ID 防混淆 - 角色层级继承 :
InheritsRole<Role, Target>递归检查父子关系 - 权限 DSL:结合所有类型工具,提供编译期即可验证的权限策略语言
1.0 里程碑:AccessGuard 从 v0.1 的基础枚举类型到 v1.0 的类型级权限计算 DSL,完成了从"类型标注辅助"到"类型即安全机制"的质变。前端权限系统的安全性不再仅仅依赖开发者的细心和代码审查------编译器成为安全的第一道防线。
本期专栏更新说明
本文为《TypeScript 从入门到精通》专栏第一季(AccessGuard 贯穿案例)进阶篇第六篇,标志着 AccessGuard 1.0 的发布和进阶篇的完结。进阶篇共 6 篇文章(v0.5--v1.0),从 ABAC 属性模型开始,经历条件类型引擎、映射类型编辑器、RBAC+ABAC 融合、模板字面量可视化,最终到达类型级权限计算 DSL。下一阶段将进入 高级篇(6 篇),主题包括声明文件与 npm 发布、Zustand 状态管理类型设计、OpenAPI 类型生成、TSConfig 深度配置、泛型表单系统和 i18n 类型安全。一次订阅,永久持续更新。第一季完结后将开启第二季,以全新贯穿案例重新从入门螺旋。
参考资料
- TypeScript 6.0 Release Notes --- TypeScript 官方 6.0 发布说明(2026 年 6 月)
- Type-Level TypeScript --- Gabriel Vergnaud 的 TypeScript 类型级编程在线课程
- Branded Types --- Learning TypeScript --- Josh Goldberg 的品牌类型深度文章
- Building a robust permissions system in TypeScript --- Xetera 的 TypeScript 权限系统构建实践
- TypeScript at scale in 2026 --- LogRocket 的 2026 大规模 TypeScript 最佳实践
- Microsoft/TypeScript#202 --- Support nominal type matching --- TypeScript 官方名义类型讨论
- type-challenges --- TypeScript 类型挑战社区(含本文介绍的五道困难级题目变体)
- Effect TS --- Branded Types --- Effect 框架的品牌类型文档