TypeScript中如何定义未入库用户ID的自定义类型?
异步校验下的NewUserId类型约束方案
因为TypeScript不支持异步类型守卫,没法直接通过类型守卫把ValidUserId转换成NewUserId,下面是几个实用的替代方案,核心思路是通过异步流程强制校验,并在类型层面区分已校验和未校验的ID。
方案1:品牌类型+异步工厂函数
先通过「品牌类型」给NewUserId做独特标识,避免和ValidUserId混淆,再用异步工厂函数封装校验逻辑,只有校验通过才能生成NewUserId实例。
类型定义
// 基础合法ID类型(比如数据库允许的ID格式) type ValidUserId = string | number; // 用品牌标识区分:仅表示未存在于数据库的ID type NewUserId = ValidUserId & { __brand: "NewUserId" };
异步工厂函数
// 模拟数据库校验函数 async function checkUserIdExists(id: ValidUserId): Promise<boolean> { // 实际业务中替换为数据库查询逻辑 return await db.exists("users", { id }); } // 生成NewUserId的唯一入口:必须通过校验才能拿到合法的NewUserId async function createNewUserId(id: ValidUserId): Promise<NewUserId | null> { const exists = await checkUserIdExists(id); if (exists) return null; // ID已存在,返回null或抛出错误 // 类型断言:此时已确认ID是新的,安全转换为NewUserId return id as NewUserId; }
业务函数改造
把doSomethingWithUser的参数改为NewUserId,强制调用方必须传入已校验的ID:
async function doSomethingWithUser(id: NewUserId) { // 这里无需再校验ID存在性,直接执行添加逻辑 await db.insert("users", { id }); }
调用方式
async function handleAddUserRequest(rawId: string) { // 先做格式校验,转换为ValidUserId const validId = parseValidUserId(rawId); if (!validId) throw new Error("Invalid user ID format"); // 尝试获取NewUserId const newUserId = await createNewUserId(validId); if (!newUserId) throw new Error("User ID already exists"); // 此时传入doSomethingWithUser不会有类型错误 await doSomethingWithUser(newUserId); }
这个方案的优势是类型约束严格,直接传ValidUserId给doSomethingWithUser会触发TypeScript错误,从根源上避免遗漏校验。
方案2:类封装校验逻辑
用类来封装NewUserId,通过静态异步方法完成校验并创建实例,类型区分更直观。
类定义与校验
type ValidUserId = string | number; class NewUserId { private constructor(public readonly value: ValidUserId) {} // 静态工厂方法:异步校验后返回实例 static async create(id: ValidUserId): Promise<NewUserId | null> { const exists = await checkUserIdExists(id); if (exists) return null; return new NewUserId(id); } }
业务函数与调用
async function doSomethingWithUser(id: NewUserId) { await db.insert("users", { id: id.value }); } async function handleAddUser(rawId: string) { const validId = parseValidUserId(rawId); if (!validId) throw new Error("Invalid ID format"); const newUserId = await NewUserId.create(validId); if (!newUserId) throw new Error("ID exists"); await doSomethingWithUser(newUserId); }
这个方案的好处是封装性更强,可以在类里扩展更多逻辑(比如ID格式二次校验),同时类型层面的区分也很明确。
方案3:函数重载+内部校验(弱约束)
如果不想改变现有函数的调用方式,可以用函数重载区分已校验和未校验的ID,但这种方式是内部自动补全校验,而非强制调用方提前校验,适合兼容旧代码场景。
type ValidUserId = string | number; type NewUserId = ValidUserId & { __brand: "NewUserId" }; // 重载1:接受未校验的ValidUserId,内部自动做异步校验 async function doSomethingWithUser(id: ValidUserId): Promise<void>; // 重载2:接受已校验的NewUserId,直接执行逻辑 async function doSomethingWithUser(id: NewUserId): Promise<void>; // 函数实现 async function doSomethingWithUser(id: ValidUserId | NewUserId) { // 检查是否是已校验的NewUserId if ("__brand" in id && id.__brand === "NewUserId") { await db.insert("users", { id }); return; } // 未校验的ID,先执行异步校验 const exists = await checkUserIdExists(id); if (exists) throw new Error("User ID already exists"); await db.insert("users", { id }); }
这个方案的缺点是无法强制调用方提前做校验,只能在函数内部兜底,但好处是兼容原有调用方式,不需要大幅修改代码。
内容的提问来源于stack exchange,提问作者bgfxc4
相关产品推荐
相关产品推荐

