How can I create a compiler error if a variable is NOT of type never?

52 views Asked by At

Let's say I have a list of conditionals and I want to handle every possible value of type. If I added a new type in the future and I forget to handle it, I want an error - at least a run-time error, but ideally a compiler error so I catch my mistake before I deploy.

How can I assert that a variable is of 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}".`)
  }
}
2

There are 2 answers

0
jcalz On BEST ANSWER

You can introduce a never-returning function which only accepts an argument of the never type, meaning that the compiler will only be happy if it doesn't actually expect the function to get called. This function is usually called assertNever(). Here's one way to write it:

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

If it does get called then you'll get a runtime error. Now the following compiles as expected:

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

But if we add a new job type:

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

now it fails, also as expected:

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}".`
  );
}

The compiler error warns you that you haven't handled the new type, which should hopefully prompt you to add another if/else statement.

Playground link to code

0
d2vid On

You can extend the Error class to create an error type whose constructor expects an argument of type never:

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

Then you throw the error if the code is ever reached, and if job isn't never it'll also raise a compiler error:

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