Skip to main content

TypeScript Literal Types

Introduction

TypeScript's type system is designed to help you catch errors early during development. One powerful feature that enhances this capability is literal types. Unlike regular types like string or number, which accept any value of that type, literal types allow you to specify the exact values that are allowed.

In this tutorial, we'll explore how literal types work, why they're useful, and how you can use them to write more precise and safer TypeScript code.

What Are Literal Types?

Literal types allow you to specify the exact value a variable can have, rather than just its general type. For example, instead of saying a variable is of type string, you can say it must be specifically the string "error" or "success".

TypeScript supports literal types for:

  • Strings
  • Numbers
  • Booleans

String Literal Types

Let's start with string literals, which are the most common form of literal types.

typescript
// Regular string type - any string value is allowed
let message: string;
message = "Hello"; // OK
message = "Goodbye"; // OK

// String literal type - only the exact value "error" is allowed
let errorMessage: "error";
errorMessage = "error"; // OK
errorMessage = "Error"; // Error: Type '"Error"' is not assignable to type '"error"'

Practical Use Case: Status Codes

String literals are perfect for representing fixed values like status codes:

typescript
// Define our status literals
type Status = "pending" | "fulfilled" | "rejected";

// Use them in a function
function handleStatus(status: Status) {
switch (status) {
case "pending":
console.log("Operation is in progress...");
break;
case "fulfilled":
console.log("Operation completed successfully!");
break;
case "rejected":
console.log("Operation failed.");
break;
}
}

// These work fine
handleStatus("pending");
handleStatus("fulfilled");

// This would cause a compile-time error
// handleStatus("complete"); // Error: Argument of type '"complete"' is not assignable to parameter of type 'Status'

Number Literal Types

Similar to string literals, number literal types specify exact numerical values:

typescript
// Only the exact value 404 is allowed
let notFoundCode: 404;
notFoundCode = 404; // OK
// notFoundCode = 400; // Error: Type '400' is not assignable to type '404'

// Union of number literals for HTTP status codes
type HttpSuccessCode = 200 | 201 | 204;
let successCode: HttpSuccessCode;
successCode = 200; // OK
successCode = 201; // OK
// successCode = 404; // Error: Type '404' is not assignable to type 'HttpSuccessCode'

Practical Use Case: Configuration Options

Number literals are great for defining valid configuration values:

typescript
// Define valid zoom levels for a map
type ZoomLevel = 1 | 2 | 3 | 4 | 5;

function setMapZoom(zoom: ZoomLevel) {
console.log(`Setting map zoom to level ${zoom}`);
// Implementation details...
}

setMapZoom(3); // OK
// setMapZoom(6); // Error: Argument of type '6' is not assignable to parameter of type 'ZoomLevel'

Boolean Literal Types

Boolean literal types are less commonly used individually, but they can be useful in specific scenarios:

typescript
type True = true;
type False = false;

// A function that only accepts true
function activateFeature(activate: true) {
console.log("Feature activated!");
}

activateFeature(true); // OK
// activateFeature(false); // Error: Argument of type 'false' is not assignable to parameter of type 'true'

Combining Literal Types with Union Types

The real power of literal types emerges when combined with union types, allowing you to specify a discrete set of allowed values:

typescript
// Define a union of string literal types
type Direction = "north" | "south" | "east" | "west";

function move(direction: Direction, steps: number) {
console.log(`Moving ${steps} steps ${direction}`);
}

move("north", 3); // OK
// move("northeast", 3); // Error: Argument of type '"northeast"' is not assignable to parameter of type 'Direction'

Real-World Examples

Example 1: API Response Handlers

When working with APIs, you often need to handle different response types:

typescript
type ApiResponseType = "json" | "text" | "blob" | "arrayBuffer";

async function fetchData(url: string, responseType: ApiResponseType) {
const response = await fetch(url);

switch (responseType) {
case "json":
return await response.json();
case "text":
return await response.text();
case "blob":
return await response.blob();
case "arrayBuffer":
return await response.arrayBuffer();
}
}

// Usage
fetchData("https://api.example.com/data", "json")
.then(data => console.log(data));

Example 2: Creating a Type-Safe Event System

typescript
// Define allowed event types
type EventType = "click" | "hover" | "scroll" | "keypress";

// Define event handler type
interface EventHandler {
type: EventType;
handler: (event: any) => void;
}

class EventManager {
private handlers: EventHandler[] = [];

// Only accept predefined event types
addEventHandler(type: EventType, handler: (event: any) => void) {
this.handlers.push({ type, handler });
console.log(`Registered handler for ${type} event`);
}

triggerEvent(type: EventType, event: any) {
this.handlers
.filter(h => h.type === type)
.forEach(h => h.handler(event));
}
}

// Usage
const eventManager = new EventManager();
eventManager.addEventHandler("click", (e) => console.log("Click event:", e));
eventManager.triggerEvent("click", { x: 100, y: 200 });

// This would cause a compile-time error
// eventManager.addEventHandler("doubleclick", (e) => console.log(e)); // Error: Argument of type '"doubleclick"' is not assignable to parameter of type 'EventType'

Example 3: Configuration Options with Mixed Types

Literal types can be mixed with other types to create complex configurations:

typescript
type Theme = "light" | "dark" | "system";
type FontSize = "small" | "medium" | "large" | number;

interface AppConfig {
theme: Theme;
fontSize: FontSize;
notifications: boolean;
refreshRate: 30 | 60 | 120; // Only these specific refresh rates are valid
}

function updateConfig(config: Partial<AppConfig>) {
console.log("Updating config with:", config);
// Implementation details...
}

// All of these are valid
updateConfig({ theme: "dark" });
updateConfig({ fontSize: "large" });
updateConfig({ fontSize: 16 }); // Custom numeric font size is allowed
updateConfig({ refreshRate: 60 });

// These would cause errors
// updateConfig({ theme: "blue" }); // Error: Type '"blue"' is not assignable to type 'Theme'
// updateConfig({ refreshRate: 90 }); // Error: Type '90' is not assignable to type '30 | 60 | 120'

Template Literal Types

TypeScript 4.1 introduced template literal types, which extend the concept of string literals by allowing you to create new string literal types by combining existing ones:

typescript
type Color = "red" | "green" | "blue";
type Size = "small" | "medium" | "large";

// Creates all combinations: "small-red", "small-green", "small-blue", "medium-red", etc.
type ColoredSize = `${Size}-${Color}`;

function createButton(size: ColoredSize) {
console.log(`Creating a ${size} button`);
}

createButton("small-red"); // OK
createButton("large-blue"); // OK
// createButton("huge-yellow"); // Error: Argument of type '"huge-yellow"' is not assignable to parameter of type 'ColoredSize'

The as const Assertion

The as const assertion is closely related to literal types. It lets you create read-only objects with literal type properties:

typescript
// Without as const, this has type { name: string, role: string }
const user = { name: "Alice", role: "admin" };

// With as const, this has type { readonly name: "Alice", readonly role: "admin" }
const userReadonly = { name: "Alice", role: "admin" } as const;

// This works because 'role' is just a string
user.role = "user"; // OK

// This causes an error because role is specifically the literal "admin"
// userReadonly.role = "user"; // Error: Cannot assign to 'role' because it is a read-only property.

Using Literal Types With Discriminated Unions

One of the most powerful uses of literal types is in discriminated unions, where a literal property helps TypeScript narrow down the type:

typescript
interface SuccessResponse {
status: "success";
data: any;
}

interface ErrorResponse {
status: "error";
error: string;
}

type ApiResponse = SuccessResponse | ErrorResponse;

function handleResponse(response: ApiResponse) {
// TypeScript knows which properties are available based on the status
if (response.status === "success") {
// TypeScript knows this is SuccessResponse
console.log(`Success with data: ${JSON.stringify(response.data)}`);
} else {
// TypeScript knows this is ErrorResponse
console.log(`Error: ${response.error}`);
}

// This would cause an error because TypeScript knows that in the else case,
// response.data doesn't exist
// if (response.status === "error") {
// console.log(response.data); // Error: Property 'data' does not exist on type 'ErrorResponse'
// }
}

Use Cases for Literal Types

Literal types are particularly useful for:

  1. API Parameters: Ensuring functions receive only valid parameters
  2. Configuration Objects: Restricting configuration options to valid choices
  3. State Machines: Defining valid states and transitions
  4. Discriminated Unions: Creating powerful, type-safe unions of different types
  5. Message Types: Defining the exact format of messages in communication protocols

Summary

Literal types are a powerful feature of TypeScript that allow you to define more precise types by specifying exact values rather than just general categories like string or number. They work especially well when combined with union types and discriminated unions.

Key points to remember:

  • Literal types specify exact values ("error", 200, true) instead of general types
  • They can be combined with union types to create a limited set of allowed values
  • They work well with discriminated unions to create type-safe conditional logic
  • Template literal types allow you to combine string literals in powerful ways
  • The as const assertion helps create objects with literal type properties

By using literal types effectively, you can catch more errors at compile-time rather than runtime, leading to more robust and maintainable TypeScript code.

Exercises

  1. Create a function that only accepts valid CSS color names as literal types (limit to 5-10 colors).
  2. Implement a state machine for a traffic light using literal types for states and transitions.
  3. Define a config object type for a chart library that uses literal types for chart types, sizes, and animation options.
  4. Create a discriminated union type for different shapes (circle, rectangle, triangle) with appropriate properties for each.
  5. Use template literal types to create a validation function that accepts specific patterns of IDs.

Additional Resources



If you spot any mistakes on this website, please let me know at [email protected]. I’d greatly appreciate your feedback! :)