网站搜索

typescript - 如果一个变量不是 never 类型,我如何创建一个编译器错误?


假设我有一个条件列表,并且我想处理 type 的每一个可能值。如果将来我添加了一个新的 type,但忘记处理它,我希望出现错误——至少是运行时错误,但理想情况下最好是编译器错误,这样我可以在部署之前发现我的错误。

我如何断言一个变量的类型是 never?

type Job = { type: 'add'; payload: any } | { type: 'send'; payload: any }

const handleJob = (job: Job) => {
  const add = (job: Job) => {
    console.log(job)
  }
  const send = (job: Job) => {
    console.log(job)
  }

  if (job.type === 'add') {
    add(job)
  } else if (job.type === 'send') {
    send(job)
  } else {
    // `job` is actually type `never` here,
    // this error won't ever be thrown unless additional strings are added to the `Job.type` schema.
    throw new Error(`Unhandled job.type "${(job as Job).type}".`)
  }
}

答案#1

你可以引入一个只接受 never 类型参数、且不会返回的函数,这意味着编译器只会在它实际上不期望该函数被调用时才会满意。这个函数通常被称为 assertNever()。下面是一种编写它的方式:

function assertNever(x: never, msg?: string): never {
  throw new Error(msg ?? "unexpected value " + String(x));
}

如果它确实被调用,那么你将会遇到运行时错误。现在下面的代码按预期进行编译:

if (job.type === 'add') {
  add(job)
} else if (job.type === 'send') {
  send(job)
} else {
  assertNever(job, // <-- okay    
    `Unhandled job.type "${(job as Job).type}".`
  );
}

但是如果我们添加一种新的工作类型:

type Job =
  { type: 'add'; payload: any } |
  { type: 'send'; payload: any } | 
  { type: 'explode' }; // add this

现在它失败了,也在预料之中:

if (job.type === 'add') {
  add(job)
} else if (job.type === 'send') {
  send(job)
} else {
  assertNever(job, // error!   
    // -----> ~~~
    // Argument of type '{ type: "explode"; }' is 
    // not assignable to parameter of type 'never'.
    `Unhandled job.type "${(job as Job).type}".`
  );
}

编译器错误会警告你尚未处理新类型,希望这能促使你添加另一个 if/else 语句。

代码的 Playground 链接

答案#2

你可以扩展 Error 类来创建一个错误类型,其构造函数期望一个类型为 never 的参数:

class NeverError extends Error {
  constructor(check: never) {
    super(`NeverError received unexpected value ${check}, type should have been never.`)
    this.name = 'NeverError'
  }
}

然后,如果代码被执行到,你就抛出错误,并且如果 job 不是 never,它也会引发编译器错误:

  if (job.type === 'add') {
    add(job)
  } else if (job.type === 'send') {
    send(job)
  } else {
    throw new NeverError(job)
  }