You need to enable JavaScript to run this app.
优惠活动
大模型
产品
解决方案
定价
更多

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

相关产品推荐
方舟 Agent Plan

超全模态模型 × Harness 升级,最新支持 Deepseek-V4.1-Flash、GLM-5.3 系列、Doubao-Seedream-5.0-pro、Kimi-K3 (部分), 限时 9.9 元起

最近更新时间:2026.08.02 02:10:15