【进阶】AccessGuard v1.0:高级类型体操 — TypeScript 类型安全的权限 DSL 深度实战

【进阶】AccessGuard v1.0:高级类型体操 --- TypeScript 类型安全的权限 DSL 深度实战

目录

前言

核心痛点 :在构建企业级权限系统时,权限检查逻辑的错误(如忘记 return early、ID 类型混用、角色层级误判)是生产环境中最隐蔽也最危险的安全漏洞。传统方案依赖运行时的 if 检查和代码审查,但人总会犯错。TypeScript 的类型系统是图灵完备的------我们可以让编译器在 IDE 中就发现权限配置错误,实现"编译不过即权限不对"的安全保障。

前置知识:需要掌握 TypeScript 泛型编程、条件类型(conditional types)、模板字面量类型、映射类型、递归类型、infer 关键字、索引访问类型。如果你已经跟随本系列前九篇文章逐步构建了 AccessGuard,本文的知识衔接将非常顺畅。

系列阶段:进阶篇第 6 篇(6/6)。本文是进阶篇的收官之作,标志着 AccessGuard 从 v0.9 升级到 v1.0,RBAC + ABAC 融合引擎的类型体系达到生产级完备状态。

收获能力:读完本文你将掌握:

  1. 类型级权限计算------用条件类型在编译期判断权限是否满足
  2. AllOf/AnyOf 类型安全组合------类型级 AND/OR 逻辑运算实现复杂权限约束
  3. 品牌类型(Branded Types)------在类型层面区分 RoleId/UserId/PermissionId 等语义不同但结构相同的 ID
  4. 类型级角色层级检查------用递归条件类型实现"父角色自动包含子角色权限"的编译期验证
  5. 类型安全的权限 DSL------设计一套编译期即可检查的权限领域特定语言
  6. 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 最核心的类型工具------在编译期判断一个权限集合是否包含某个特定权限,返回字面量类型 truefalse,而非宽泛的 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

生产适用场景

  1. 企业级权限管理系统:多角色、多资源类型、细粒度权限控制,权限矩阵复杂度超过 100 种权限
  2. SaaS 多租户平台:租户隔离 + 租户内角色管理,需要类型层面区分不同租户的 ID 空间
  3. 合规敏感应用:金融、医疗、政务等需要审计追溯的领域,权限检查必须有编译期强保证

禁忌场景

  1. 快速原型开发:权限需求尚未固化的早期阶段,过度类型化会拖慢迭代速度
  2. 权限逻辑极其简单的应用:只有 admin/user 两种角色的系统,类型体操收益不足以覆盖成本
  3. 非 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 类型安全。一次订阅,永久持续更新。第一季完结后将开启第二季,以全新贯穿案例重新从入门螺旋。

参考资料