typescripttypescript-never

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


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

Solution

  • 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