TypeScript Literal Types
A literal type is a type that allows exactly one value — for example the string "north" or the number 1 — instead of any string or any number.
Understanding Literal Types in TypeScript
Literal types let you say exactly which values a variable may hold. They are more precise than the broad types string, number or boolean.
They are the building blocks for precise, type-safe code.
Key concepts:
- String literals: exact string values like
"success" | "error" - Numeric literals: specific numbers like
1 | 2 | 3 - Boolean literals: either
trueorfalse - Template literal types: string literal types built with template string syntax
Common use cases:
- Defining a fixed set of allowed values
- Creating discriminated unions
- Type-safe event handling
- Typing API responses
- Configuration objects
String Literal Types
A string literal type stands for one specific string value. Several of them are usually combined with | into a union of allowed values.
Example
direction may only hold one of four strings. Any other string, such as "northeast", is a compile error, both when assigning to the variable and when calling move.
direction with the type "north" | "south" | "east" | "west", assign "north" and then "south", and leave commented-out assignments of "northeast" and "up" with the errors they would cause. Write a move function taking the same literal union that logs Moving <direction>, call it with "east", and leave move("up") commented out as an invalid call.
Output: TypeScript file and captured output
// A variable with a string literal type
let direction: "north" | "south" | "east" | "west";
// Valid assignments
direction = "north";
direction = "south";
// Invalid assignments would cause errors
// direction = "northeast"; // Error: Type '"northeast"' is not assignable to type '"north" | "south" | "east" | "west"'
// direction = "up"; // Error: Type '"up"' is not assignable to type '"north" | "south" | "east" | "west"'
// Using string literal types in functions
function move(direction: "north" | "south" | "east" | "west") {
console.log(`Moving ${direction}`);
}
move("east"); // Valid
// move("up"); // Error: Argument of type '"up"' is not assignable to parameter of type...
Numeric Literal Types
Just like string literals, numeric literal types stand for specific number values.
Example
diceRoll accepts only the numbers 1 to 6. Because Math.random() returns a plain number, rollDice needs a type assertion to promise it returns one of the six literals. The printed roll changes on every run.
diceRoll with the type 1 | 2 | 3 | 4 | 5 | 6, assign 1 and then 6, and leave commented-out assignments of 0, 7 and 2.5 with their errors. Write rollDice() whose return type is the same union and which returns Math.floor(Math.random() * 6) + 1 cast to that union, then log You rolled a <result> (the number is random on each run).
Output: TypeScript file and captured output
// A variable with a numeric literal type
let diceRoll: 1 | 2 | 3 | 4 | 5 | 6;
// Valid assignments
diceRoll = 1;
diceRoll = 6;
// Invalid assignments would cause errors
// diceRoll = 0; // Error: Type '0' is not assignable to type '1 | 2 | 3 | 4 | 5 | 6'
// diceRoll = 7; // Error: Type '7' is not assignable to type '1 | 2 | 3 | 4 | 5 | 6'
// diceRoll = 2.5; // Error: Type '2.5' is not assignable to type '1 | 2 | 3 | 4 | 5 | 6'
// Using numeric literal types in functions
function rollDice(): 1 | 2 | 3 | 4 | 5 | 6 {
return Math.floor(Math.random() * 6) + 1 as 1 | 2 | 3 | 4 | 5 | 6;
}
const result = rollDice();
console.log(`You rolled a ${result}`);
Boolean Literal Types
Boolean literal types are used less often, since there are only two boolean values, but they are useful in some cases — for example to say a function always returns true.
Example
alwaysSucceed must return the literal true. SuccessFlag and FailureFlag mix boolean, string and numeric literals, and processResult tells them apart with simple equality checks.
type YesOnly = true and an alwaysSucceed() function whose return type is the literal true. Then declare SuccessFlag = true | "success" | 1 and FailureFlag = false | "failure" | 0, and a processResult function taking either that logs Operation succeeded when the value is true, "success" or 1, and Operation failed otherwise. Call it with true, "success", 1 and false.
Output: TypeScript file and captured output
// A type that can only be the literal value 'true'
type YesOnly = true;
// A function that must return true
function alwaysSucceed(): true {
// Always returns the literal value 'true'
return true;
}
// Boolean literal combined with other types
type SuccessFlag = true | "success" | 1;
type FailureFlag = false | "failure" | 0;
function processResult(result: SuccessFlag | FailureFlag) {
if (result === true || result === "success" || result === 1) {
console.log("Operation succeeded");
} else {
console.log("Operation failed");
}
}
processResult(true); // "Operation succeeded"
processResult("success"); // "Operation succeeded"
processResult(1); // "Operation succeeded"
processResult(false); // "Operation failed"
Literal Types with Objects
Literal types can be used as property types inside object types. This gives you very specific object shapes.
Example
HTTPSuccess and HTTPError restrict status and statusText to a few literal values each. A plain range check on status does not narrow the union, so the example uses a small isSuccess type guard; inside each branch data or error can then be used safely.
HTTPSuccess (status: 200 | 201 | 204, statusText: "OK" | "Created" | "No Content", data: any) and HTTPError (status: 400 | 401 | 403 | 404 | 500, the matching statusText literals, error: string), plus HTTPResponse as their union. Write an isSuccess type guard (response is HTTPSuccess) that checks the status is in the 200 range, and a handleResponse function that uses it to log Success: <statusText> and the data for a success, and otherwise logs Error <status>: <statusText> and Message: <error>. Call it with a 200 "OK" response carrying { username: "john_doe", email: "[email protected]" } and a 404 "Not Found" response with the error User not found in database.
Output: TypeScript file and captured output
// Object with literal property values
type HTTPSuccess = {
status: 200 | 201 | 204;
statusText: "OK" | "Created" | "No Content";
data: any;
};
type HTTPError = {
status: 400 | 401 | 403 | 404 | 500;
statusText: "Bad Request" | "Unauthorized" | "Forbidden" | "Not Found" | "Internal Server Error";
error: string;
};
type HTTPResponse = HTTPSuccess | HTTPError;
// A type guard is needed here: a numeric range check alone does not narrow the union
function isSuccess(response: HTTPResponse): response is HTTPSuccess {
return response.status >= 200 && response.status < 300;
}
function handleResponse(response: HTTPResponse) {
if (isSuccess(response)) {
console.log(`Success: ${response.statusText}`);
console.log(response.data);
} else {
console.log(`Error ${response.status}: ${response.statusText}`);
console.log(`Message: ${response.error}`);
}
}
// Example usage
const successResponse: HTTPSuccess = {
status: 200,
statusText: "OK",
data: { username: "john_doe", email: "[email protected]" }
};
const errorResponse: HTTPError = {
status: 404,
statusText: "Not Found",
error: "User not found in database"
};
handleResponse(successResponse);
handleResponse(errorResponse);
Template Literal Types
Since TypeScript 4.1 you can build new string literal types by combining existing ones with template string syntax, using backticks and ${...} placeholders. Every combination of the placeholder unions becomes a member of the new union.
Example
The example goes from a simple DirectionAndDistance union to more advanced uses: Capitalize to build event names, a mapped type that creates getter names, infer to pull parameter names out of a route string, a CssValue type of number plus unit, and a typed SQL query builder. The file ends with export {} so its local EventTarget type does not clash with the built-in DOM type.
Direction and Distance unions into DirectionAndDistance (`${Direction}-${Distance}`) and assign valid routes such as "north-5km" to a route variable. Then build EventName with Capitalize, a GetterName<T>/UserGetters mapped type over a User type, a recursive ExtractRouteParams<T> that pulls :param names out of a path with infer, a CssValue type of `${number}${CssUnit}`, an ApiUrl type, and a query<T extends Table> function whose return type is `SELECT * FROM ${T}${string}` called for the users table with a WhereCondition. Leave invalid assignments commented out. Nothing is printed.
Add export {}; at the end so the local EventTarget type does not clash with the built-in DOM global.
Output: TypeScript file and captured output
// Basic template literals
type Direction = "north" | "south" | "east" | "west";
type Distance = "1km" | "5km" | "10km";
// Using template literals to combine them
type DirectionAndDistance = `${Direction}-${Distance}`;
// "north-1km" | "north-5km" | "north-10km" | "south-1km" | ...
let route: DirectionAndDistance;
route = "north-5km"; // Valid
route = "west-10km"; // Valid
// route = "north-2km"; // Error
// route = "5km-north"; // Error
// Advanced string manipulation
type EventType = "click" | "hover" | "scroll";
type EventTarget = "button" | "link" | "div";
type EventName = `on${Capitalize<EventType>}${Capitalize<EventTarget>}`;
// "onClickButton" | "onClickLink" | "onClickDiv" | ...
// Dynamic property access
type User = {
id: number;
name: string;
email: string;
createdAt: Date;
};
type GetterName<T> = `get${Capitalize<string & keyof T>}`;
type UserGetters = {
[K in keyof User as GetterName<User>]: () => User[K];
};
// { getId: () => number; getName: () => string; ... }
// String pattern matching
type ExtractRouteParams<T extends string> =
T extends `${string}:${infer Param}/${infer Rest}`
? Param | ExtractRouteParams<Rest>
: T extends `${string}:${infer Param}`
? Param
: never;
type Params = ExtractRouteParams<"/users/:userId/posts/:postId">; // "userId" | "postId"
// CSS units and values
type CssUnit = 'px' | 'em' | 'rem' | '%' | 'vh' | 'vw';
type CssValue = `${number}${CssUnit}`;
let width: CssValue = '100px'; // Valid
let height: CssValue = '50%'; // Valid
// let margin: CssValue = '10'; // Error
// let padding: CssValue = '2ex'; // Error
// API versioning
type ApiVersion = 'v1' | 'v2' | 'v3';
type Endpoint = 'users' | 'products' | 'orders';
type HttpMethod = 'GET' | 'POST' | 'PUT' | 'DELETE';
type ApiUrl = `https://api.example.com/${ApiVersion}/${Endpoint}`;
// Complex example: Dynamic SQL query builder
type Table = 'users' | 'products' | 'orders';
type Column<T extends Table> =
T extends 'users' ? 'id' | 'name' | 'email' | 'created_at' :
T extends 'products' ? 'id' | 'name' | 'price' | 'in_stock' :
T extends 'orders' ? 'id' | 'user_id' | 'total' | 'status' : never;
type WhereCondition<T extends Table> = {
[K in Column<T>]?: {
equals?: any;
notEquals?: any;
in?: any[];
};
};
function query<T extends Table>(
table: T,
where?: WhereCondition<T>
): `SELECT * FROM ${T}${string}` {
// Implementation would build the query
return `SELECT * FROM ${table}` as const;
}
// Usage
const userQuery = query('users', {
name: { equals: 'John' },
created_at: { in: ['2023-01-01', '2023-12-31'] }
});
// Type: "SELECT * FROM users WHERE ..."
// Make this file a module so the local EventTarget does not clash with the built-in global one
export {};
Best Practices
Do:
- Use literal types for fixed sets of values (enum-like options, configuration values)
- Combine them with union types for better type safety
- Use template literal types for string pattern matching
- Let type inference work for you where possible
- Document what each literal type means
Don't:
- Overuse literal types when a more general type would be better
- Create extremely large union types that slow down type checking
- Use string literals when an enum would be a better fit
Performance Considerations
- Large union types can slow down type checking
- Complex template literal types can increase compile time
- Use type aliases to name complex literal types instead of repeating them
- Keep TypeScript's recursion depth limits in mind for recursive template literal types