TypeScript exhaustive switch on /v1/videos statuses fails the build

A never-typed default case makes a new video job status a compile error, and a runtime parser rejects strings the types do not know. 26 lines of TypeScript.

5 min readSume
All posts

Model the five /v1/videos job statuses as a string union, switch on them with a never check in the default branch, and add a runtime parser for the value you read from the wire. Then a sixth status, whenever one appears, fails the compile in your own code instead of falling through a switch and leaving a job in limbo. The sample is 26 lines of TypeScript.

Types do not exist at runtime. A response typed as VideoStatus can still carry any string, so the parser is the other half of the pattern: it throws on a value you have not handled, which turns a silent hang into a loud error.

The statuses

The generic jobs surface has a different vocabulary: queued, processing, completed, failed, and canceled. Keep one union for each surface and do not share a type between them.

Job statuses on /v1/videos (Sume docs read 2026-10-08)
StatusMeaningNext step in the sample
pendingSubmitted and in the queuewait
in_progressGeneration in progresswait
completedVideo can be downloadeddownload
failedGeneration failed, see the error fieldstop
cancelledCanceled, did not completestop

The code

assertNever takes a never, so the compiler only accepts the call when every case above it has returned. Add a sixth member to the union and nextStep stops compiling until you handle it.

type VideoStatus = "pending" | "in_progress" | "completed" | "failed" | "cancelled";

function assertNever(value: never): never {
  throw new Error(`unhandled video status: ${String(value)}`);
}

export function nextStep(status: VideoStatus): "wait" | "download" | "stop" {
  switch (status) {
    case "pending":
    case "in_progress":
      return "wait";
    case "completed":
      return "download";
    case "failed":
    case "cancelled":
      return "stop";
    default:
      return assertNever(status);
  }
}

export function parseStatus(raw: unknown): VideoStatus {
  const known = ["pending", "in_progress", "completed", "failed", "cancelled"];
  if (typeof raw === "string" && known.includes(raw)) return raw as VideoStatus;
  throw new Error(`unknown video status from the API: ${String(raw)}`);
}

Using it in a poll loop

Do not treat wait as an error and do not retry the submit. Pending and in-progress are normal for a job that can take from 30 seconds to several minutes.

  • Read status from the polling URL response and pass it through parseStatus first.
  • Call nextStep. On wait, sleep and poll again. On download, fetch unsigned_urls[0] with your API key header.
  • On stop, read the error field of a failed job and log the request id.

A note on the SDK

If you use @sume-com/sdk, its waitForJob helper handles the generic job statuses for you and resolves for failed jobs too, so you read status, result, and error from the record instead of catching an exception. Use the union in this post when you call /v1/videos directly, where the wire format has its own spellings.

Testing the guard

Add two small tests. The first calls parseStatus with each known status and expects it back unchanged. The second calls it with a made-up string and expects a thrown error. Together they prove that the runtime half of the pattern works, and the compiler proves the other half.

To see the compile-time half work, add a sixth member such as paused to the union in a scratch branch and run your type check. The assertNever call fails to compile, and the error points at the exact switch you need to update. That one failing build is the point of the pattern.

Keep the union next to the code that reads the wire, and keep a comment with the docs link and read date, so the next person knows where the list came from and when it was last checked.

Sources

Related posts

More in Developers

All Developers posts

Written by Sume