← 中文资源

简体中文版本

Unknown 类型守卫收窄

用可组合的类型守卫逐层收窄 unknown:判别字段 switch 穷尽分支、字段存在性与类型逐一验证,断言失败精确抛错。

阅读英文原文

示例说明

any 会关闭类型检查让脏数据长驱直入,unknown 才是外部数据的正确入口——它强制你先证明再使用。守卫函数的返回类型谓词让编译器在 true 分支把值视为目标形态;hasKeys 用泛型键列表同时完成存在性检查与键类型收窄,与 switch 判别分支叠加后每个分支内部都拿到精确类型。

技术要点

  • 类型谓词让守卫结果驱动收窄
  • 泛型键校验同时收窄键集合
  • default 分支兜住新增判别值

代码解读

isRecord 排除 null 与数组;hasKeys 在其上确认必需键存在。parseOutcome 按 status 分支:成功分支验证 data 是字符串后返回窄化对象,失败分支验证两个再返回,default 把未识别的判别值显式拒绝——新增状态时不会静默漏处理。

使用建议

守卫链要与真实载荷形态对齐,JSON 数字与字符串的区分要显式处理。公共守卫抽成工具库复用。收窄后的值仍建议过 schema 校验做业务约束。

代码示例

interface Success<T> { status: 'success'; data: T }
interface Failure { status: 'failure'; code: number; message: string }
type Outcome<T> = Success<T> | Failure;

function isRecord(value: unknown): value is Record<string, unknown> {
  return typeof value === 'object' && value !== null && !Array.isArray(value);
}

function hasKeys<K extends string>(
  value: unknown, ...keys: K[]
): value is Record<K, unknown> {
  return isRecord(value) && keys.every((k) => k in value);
}

function parseOutcome(raw: unknown): Outcome<string> {
  if (!hasKeys(raw, 'status')) {
    throw new TypeError('outcome requires a status field');
  }
  switch (raw.status) {
    case 'success':
      if (typeof raw.data !== 'string') {
        throw new TypeError('success outcome needs string data');
      }
      return { status: 'success', data: raw.data };
    case 'failure':
      if (!hasKeys(raw, 'code', 'message')) {
        throw new TypeError('failure outcome needs code and message');
      }
      if (typeof raw.code !== 'number' || typeof raw.message !== 'string') {
        throw new TypeError('failure fields have wrong types');
      }
      return { status: 'failure', code: raw.code, message: raw.message };
    default:
      throw new TypeError('unknown status: ' + String(raw.status));
  }
}

const ok = parseOutcome(JSON.parse('{"status":"success","data":"hello"}'));
console.log(ok.status === 'success' ? ok.data.toUpperCase() : 'n/a');