Back to Blog
Lesson 14 of the TypeScript: Typing JavaScript with Confidence course
TypeScriptAugust 1, 20264 min read

Literal Types for Status Codes: Enforcing Constraints in TypeScript

Learn how to use TypeScript Literal Types to restrict status codes to a specific set of values, preventing invalid state assignments and catching bugs early.

TypeScriptLiteral TypesType SafetyAPI DevelopmentStatus Codes
Close view of computer screen displaying HTML code with an authentication error.

Previously in this course, we explored Union Types for Flexibility in TypeScript, which allowed us to permit multiple types for a single variable. While that approach handles variety, it doesn't solve the problem of "invalid data." If you define a status as a string, any string—even "deleted-but-not-really"—becomes a valid status.

Literal Types take us a step further. They allow us to restrict a variable not just to a type (like string or number), but to a specific, hard-coded set of values.

Defining Status Literal Types

In TypeScript, a literal type is a type that represents a single, exact value. For our task API client, we don't want a task to have just any string as a status; we want it to be either 'pending', 'in-progress', or 'completed'.

Instead of using the broad string type, we define a custom type using the pipe operator (|) to combine specific string literals:

TYPESCRIPT
type TaskStatus = CE9178">'pending' | CE9178">'in-progress' | CE9178">'completed';

interface Task {
  id: number;
  title: string;
  status: TaskStatus; // This now only accepts the three strings above
}

By defining TaskStatus as a set of specific literals, we transform the status field from a "free-for-all" string into a strictly controlled constraint.

Enforcing Status Constraints

When you assign a value to a property typed as a literal, the TypeScript compiler checks that value against your defined set. If you try to assign something else, the build will fail.

TYPESCRIPT
const myTask: Task = {
  id: 1,
  title: "Finish the API client",
  status: CE9178">'pending' // Valid
};

// This will trigger a compiler error:
// Type CE9178">'"archived"' is not assignable to type CE9178">'TaskStatus'.
myTask.status = CE9178">'archived'; 

This is significantly more robust than relying on runtime checks or documentation to tell developers which status codes are valid. The editor will even provide autocomplete suggestions for the allowed values, effectively documenting your API's constraints through the type system itself.

Preventing Invalid Assignments

In a real-world API client, you'll often receive data from an external source that might contain unexpected values. Using literal types, you can create a boundary where you validate incoming data before allowing it into your application logic.

If you are interested in the semantics behind why we choose specific states for our tasks, it helps to understand HTTP Status Codes: Success Semantics for Clean API Design, which often map to the internal state transitions our client handles.

Hands-on Exercise

Update your current task API project to use a literal type for task priority.

  1. Create a new type alias named TaskPriority.
  2. Assign it these three literal values: 'low', 'medium', 'high'.
  3. Add a priority field to your Task interface using this new type.
  4. Try to create a task object with a priority of 'urgent'—verify that TypeScript throws an error.

Common Pitfalls

  • Forgetting the Union: It is easy to accidentally type type Status = 'pending' | 'completed' and then wonder why 'in-progress' is failing. Ensure every valid state is explicitly included in the union.
  • Case Sensitivity: 'Pending' (capitalized) is not the same as 'pending'. Literal types are case-sensitive. If your API sends capitalized statuses, you must include them in your type (e.g., 'Pending' | 'pending').
  • Over-generalization: Don't be tempted to fall back to string just because you want to "support future statuses." If you need to add a new status, add it to the literal union. This ensures the rest of your app remains aware of the new state.

FAQ

Can I mix numbers and strings? Yes. You could define type ResponseCode = 200 | 404 | 500.

Does this affect the runtime JavaScript? No. Literal types are erased during compilation. They exist purely to help you write code with confidence during development.

Is this better than an Enum? Enums can be confusing due to their runtime behavior. For simple status sets, literal union types are the modern, standard way to handle restricted values in TypeScript.

Recap

We've moved from loose primitives to strict, domain-specific constraints. By using Literal Types, you enforce Status Constraints that act as a safety net, ensuring your application only ever works with valid data states. This prevents bugs at the source and makes your code self-documenting.

Up next: We will look at how to use these types effectively in your logic with Type Narrowing with Conditionals.

Similar Posts