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.

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.
| Status | Meaning | Next step in the sample |
|---|---|---|
| pending | Submitted and in the queue | wait |
| in_progress | Generation in progress | wait |
| completed | Video can be downloaded | download |
| failed | Generation failed, see the error field | stop |
| cancelled | Canceled, did not complete | stop |
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
statusfrom the polling URL response and pass it throughparseStatusfirst. - Call
nextStep. Onwait, sleep and poll again. Ondownload, fetchunsigned_urls[0]with your API key header. - On
stop, read theerrorfield 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
- Unit-test your Omni price math before Oct 22: Python asserts for Sume
A runnable Python test for Sume's gemini-omni-flash-1.1 pricing: Decimal rates, the 3 to 10 second rule, and a one-cent check against the usage.cost on a poll.
- One Sume webhook route for job and run events, 204 on the rest
Route job.* and *.run.terminal events from one verified webhook handler and answer 204 for any unknown event, so a new type cannot trigger retries.
- urllib3 Retry on POST: retry a Sume submit only with a key
A urllib3 Retry config that retries 429, 502, 503 and 504 on a Sume submit, and a wrapper that refuses to send a POST without an Idempotency-Key.
- Sume video poll usage.cost: reserved while running, captured at end
The usage.cost number is the Sume billable amount: the reservation while a job runs and the captured amount once it settles. wan-3.0 at 720p, 10 s, as math.
Written by Sume