Union types, literal types, and enums
In this chapter, you learn about union types that combine multiple types, literal types that allow only specific values, and enums that group related constants.
What you learn in this chapter
- How to allow multiple types with a union type
- How to allow only specific values with a literal type
- How to use enums
- Practical type-design patterns
The content of this chapter is important for designing more robust types. Combining union types with literal types lets you strictly limit the allowed values.
Literal types
A literal type allows only a specific value.
Basic literal types
// A string literal type
let direction: 'north' = 'north';
// direction = 'south'; // Error: only 'north' can be assigned
// A numeric literal type
let statusCode: 200 = 200;
// statusCode = 404; // Error: only 200 can be assigned
// A boolean literal type
let isTrue: true = true;
// isTrue = false; // Error: only true can be assigned
The difference in type inference between let and const
// let: inferred as a broad type
let str1 = 'hello'; // the type is string
str1 = 'world'; // OK (you can assign any string value)
// const: inferred as a literal type
const str2 = 'hello'; // the type is 'hello' (a string literal type)
// str2 = 'world'; // Error: cannot reassign because it is const
as const (const assertion)
Using as const lets you fix the values of an object or array as literal types.
// A normal object
let config1 = {
apiUrl: 'https://api.example.com',
timeout: 5000
};
// The type of config1: { apiUrl: string; timeout: number; }
config1.apiUrl = 'https://api2.example.com'; // OK (changeable)
// Using as const
let config2 = {
apiUrl: 'https://api.example.com',
timeout: 5000
} as const;
// The type of config2:
// {
// readonly apiUrl: 'https://api.example.com';
// readonly timeout: 5000;
// }
// config2.apiUrl = 'https://api2.example.com'; // Error (readonly)
// config2.timeout = 3000; // Error (readonly)
The effects of as const
- All properties become readonly
- Primitive values become literal types
- Arrays become readonly tuples
// as const with an array
let arr1 = [1, 2, 3];
// The type: number[]
let arr2 = [1, 2, 3] as const;
// The type: readonly [1, 2, 3] (a readonly tuple)
// arr2.push(4); // Error (cannot change because it is readonly)
// arr2[0] = 10; // Error (cannot change because it is readonly)
Union types
A union type represents a value that is one of several types.
A basic union type
// Basic syntax: type1 | type2 | type3
let value: string | number;
value = 'Hello'; // OK (string)
value = 42; // OK (number)
// value = true; // Error (boolean is not allowed)
Combining literal types with union types
Combining literal types with union types lets you limit the allowed values.
// A type that represents direction
type Direction = 'north' | 'south' | 'east' | 'west';
let dir: Direction = 'north'; // OK
dir = 'south'; // OK
// dir = 'up'; // Error: 'up' is not allowed
// A type that represents size
type Size = 'small' | 'medium' | 'large';
let shirtSize: Size = 'medium'; // OK
// shirtSize = 'extra-large'; // Error
// HTTP status codes
type SuccessStatusCode = 200 | 201 | 204;
type ErrorStatusCode = 400 | 404 | 500;
type StatusCode = SuccessStatusCode | ErrorStatusCode;
let status: StatusCode = 200; // OK
status = 404; // OK
// status = 301; // Error
Practical examples
// Example 1: kinds of events
type MouseEventType = 'click' | 'dblclick' | 'mousedown' | 'mouseup';
type KeyboardEventType = 'keydown' | 'keyup' | 'keypress';
type EventType = MouseEventType | KeyboardEventType;
function handleEvent(eventType: EventType): void {
console.log(`Handling event: ${eventType}`);
}
handleEvent('click'); // OK
handleEvent('keydown'); // OK
// handleEvent('scroll'); // Error
// Example 2: theme and language settings
type ThemeMode = 'light' | 'dark' | 'auto';
type Language = 'ja' | 'en' | 'zh';
type AppConfig = {
theme: ThemeMode;
language: Language;
};
let config: AppConfig = {
theme: 'dark',
language: 'ja'
};
// Example 3: days of the week
type DayOfWeek = 'Sunday' | 'Monday' | 'Tuesday' | 'Wednesday' | 'Thursday' | 'Friday' | 'Saturday';
function isWeekend(day: DayOfWeek): boolean {
return day === 'Sunday' || day === 'Saturday';
}
console.log(isWeekend('Monday')); // false
console.log(isWeekend('Saturday')); // true
Type narrowing
When you use a variable of a union type, narrow the type before operating on it.
function processValue(value: string | number): void {
// as is, you can only use methods common to string and number
// console.log(value.toUpperCase()); // Error: number has no toUpperCase
// narrow the type with a typeof type guard
if (typeof value === 'string') {
// here, value can be treated as a string
console.log(value.toUpperCase());
} else {
// here, value can be treated as a number
console.log(value.toFixed(2));
}
}
processValue('hello'); // 'HELLO'
processValue(3.14159); // '3.14'
Arrays and union types
// The array elements are a union type
let mixedArray: (string | number)[] = [1, 'two', 3, 'four'];
mixedArray.push(5); // OK
mixedArray.push('six'); // OK
// mixedArray.push(true); // Error
// The array itself is a union type
let arrayOrString: number[] | string = [1, 2, 3];
arrayOrString = 'hello'; // OK
// arrayOrString = 123; // Error
Enums
An enum is a feature that groups related constants together.
The trend in 2026: avoid enum in new code
With the erasableSyntaxOnly option introduced in TypeScript 5.8+ (see Chapter 17, Modules and tsconfig), enum becomes an error. This is because enum violates the Erasable principle that "removing the types alone should leave code that runs as JavaScript," since it generates runtime code.
With Node.js type stripping (an execution method that removes only the type annotations; enabled by default without flags from 22.18 onward / on v24), enum also causes an ERR_UNSUPPORTED_TYPESCRIPT_SYNTAX error. Bun and Deno, on the other hand, run TypeScript with full transpilation, so enum works there. But if you stick to erasableSyntaxOnly, the same code runs as is on any runtime.
In new code, the recommended approach is an as const object + union-type literals (see "Modern patterns" below). Read this section's enum explanation as knowledge for reading existing code.
Numeric enums
// A basic numeric enum (values are assigned automatically from 0)
enum Direction {
North, // 0
South, // 1
East, // 2
West // 3
}
console.log(Direction.North); // 0
console.log(Direction.South); // 1
console.log(Direction.East); // 2
console.log(Direction.West); // 3
// Reverse mapping (get the name from the number)
console.log(Direction[0]); // 'North'
console.log(Direction[1]); // 'South'
Specifying initial values
// If you specify the first value, the rest auto-increment
enum Status {
Pending = 1, // 1
InProgress, // 2
Completed, // 3
Cancelled // 4
}
// Specify all values explicitly
enum HttpStatus {
OK = 200,
Created = 201,
BadRequest = 400,
NotFound = 404,
InternalError = 500
}
console.log(HttpStatus.OK); // 200
console.log(HttpStatus.NotFound); // 404
console.log(HttpStatus[200]); // 'OK'
String enums
// A string enum (you must specify all values explicitly)
enum Color {
Red = 'RED',
Green = 'GREEN',
Blue = 'BLUE'
}
console.log(Color.Red); // 'RED'
console.log(Color.Green); // 'GREEN'
// Reverse mapping is not possible (a characteristic of string enums)
// console.log(Color['RED']); // Error: 'RED' is not a member name, so it is a type error (undefined when run as JS)
The advantage of string enums
// A meaningful value is shown when debugging
enum LogLevel {
Debug = 'DEBUG',
Info = 'INFO',
Warning = 'WARNING',
Error = 'ERROR'
}
function log(level: LogLevel, message: string): void {
console.log(`[${level}] ${message}`);
}
log(LogLevel.Error, 'Something went wrong');
// Output: [ERROR] Something went wrong
// With a numeric enum, the meaning is hard to understand
enum LogLevel2 {
Debug, // 0
Info, // 1
Warning, // 2
Error // 3
}
function log2(level: LogLevel2, message: string): void {
console.log(`[${level}] ${message}`);
}
log2(LogLevel2.Error, 'Something went wrong');
// Output: [3] Something went wrong (it is hard to tell what 3 means)
Examples of using enums
// Example 1: managing application state
enum AppState {
Initializing = 'INITIALIZING',
Ready = 'READY',
Running = 'RUNNING',
Paused = 'PAUSED',
Stopped = 'STOPPED'
}
let currentState: AppState = AppState.Initializing;
function setState(newState: AppState): void {
console.log(`State changed: ${currentState} → ${newState}`);
currentState = newState;
}
setState(AppState.Ready);
setState(AppState.Running);
// Example 2: a user's permission level
enum UserRole {
Guest = 'GUEST',
User = 'USER',
Admin = 'ADMIN',
SuperAdmin = 'SUPER_ADMIN'
}
function checkPermission(role: UserRole): boolean {
return role === UserRole.Admin || role === UserRole.SuperAdmin;
}
console.log(checkPermission(UserRole.User)); // false
console.log(checkPermission(UserRole.Admin)); // true
// Example 3: days of the week
enum DayOfWeek {
Sunday,
Monday,
Tuesday,
Wednesday,
Thursday,
Friday,
Saturday
}
function isWeekendDay(day: DayOfWeek): boolean {
return day === DayOfWeek.Sunday || day === DayOfWeek.Saturday;
}
console.log(isWeekendDay(DayOfWeek.Monday)); // false
console.log(isWeekendDay(DayOfWeek.Saturday)); // true
const enum (an enum optimized at compile time)
// Using const enum
const enum Direction {
North,
South,
East,
West
}
let dir: Direction = Direction.North;
console.log(dir); // 0
// The compiled JavaScript code:
// let dir = 0 /* Direction.North */;
// console.log(dir);
Characteristics of const enum:
- The values are inlined at compile time
- The generated JavaScript code is smaller
- Reverse mapping is not possible
const enum is incompatible with isolatedModules: true
const enum does not work well with single-file compilation (isolatedModules: true), and it does not work with modern toolchains such as Babel / esbuild / swc / Vite. For the following reasons, const enum has effectively become unusable.
- Incompatible with
--isolatedModules/verbatimModuleSyntax - Also an error with
--erasableSyntaxOnly(TS 5.8+) - A
const enumincluded in a library's.d.tscannot be inlined on the consuming side
In new code, do not use const enum; choose an as const object or a normal union-type literal.
Comparing enums and union types
// Using an enum
enum SizeEnum {
Small = 'SMALL',
Medium = 'MEDIUM',
Large = 'LARGE'
}
function getPriceEnum(size: SizeEnum): number {
switch (size) {
case SizeEnum.Small:
return 100;
case SizeEnum.Medium:
return 200;
case SizeEnum.Large:
return 300;
}
}
// Using a union type
type SizeUnion = 'SMALL' | 'MEDIUM' | 'LARGE';
function getPriceUnion(size: SizeUnion): number {
switch (size) {
case 'SMALL':
return 100;
case 'MEDIUM':
return 200;
case 'LARGE':
return 300;
}
}
Which one to use
| Characteristic | enum | Union type |
|---|---|---|
| Ease of use | access like Size.Small | use a string directly like 'SMALL' |
| Amount of code | a definition is needed | just a type alias is enough |
| Generated JS | an object is generated | nothing is generated (type information only) |
| Reverse mapping | only possible for numeric enums | not possible |
| Recommendation | suited to grouping related constants | simple and lightweight |
Recommended patterns in modern TypeScript
// The union type + as const approach (recommended)
const Size = {
Small: 'SMALL',
Medium: 'MEDIUM',
Large: 'LARGE'
} as const;
type Size = typeof Size[keyof typeof Size]; // 'SMALL' | 'MEDIUM' | 'LARGE'
// Usage example
let shirtSize: Size = Size.Small; // you can use it enum-style
shirtSize = 'MEDIUM'; // you can also use a string literal
Combining with the satisfies operator (TS 4.9+)
Using satisfies lets you "check that an object satisfies a specific type" while "preserving the inferred literal type." It fills the gap in cases where a type annotation (: Type) loses the concrete type.
// ❌ With a type annotation, the concrete value-type information is lost
const sizeConfig1: Record<string, string> = {
Small: 'SMALL',
Medium: 'MEDIUM',
Large: 'LARGE',
} as const;
// sizeConfig1.Small becomes the string type
// ✅ With satisfies, you type-check while preserving the literal type
const sizeConfig2 = {
Small: 'SMALL',
Medium: 'MEDIUM',
Large: 'LARGE',
} as const satisfies Record<string, string>;
// sizeConfig2.Small stays the 'SMALL' type
// A typical pattern for a type-safe lookup table
type Size = 'SMALL' | 'MEDIUM' | 'LARGE';
const sizePrice = {
SMALL: 100,
MEDIUM: 200,
LARGE: 300,
} as const satisfies Record<Size, number>;
// When Size grows, a missing key in sizePrice is caught with a compile error
// and the type of sizePrice.SMALL stays 100 (a literal type)
This pattern appears frequently in 2026 TypeScript code. It is handy to remember as a three-step structure: "define the value → fix it with as const → check the shape with satisfies."
Try it: implement status management ★★
Implement the types and a status-change function for a task management system that meets the following requirements.
Requirements:
- Define the task statuses (a union type or an enum)
- 'pending': not started
- 'in_progress': in progress
- 'completed': completed
- 'cancelled': cancelled
- Create a
getStatusMessagefunction (it returns a Japanese message based on the status) - Create a
canTransitionfunction (it determines whether a status transition is allowed)- pending → in_progress: OK
- in_progress → completed: OK
- in_progress → cancelled: OK
- completed → any: NG (cannot change after completion)
- cancelled → any: NG (cannot change after cancellation)
Hint
- Define the status as
type TaskStatus = 'pending' | 'in_progress' | 'completed' | 'cancelled'; getStatusMessagereturns the message for each status with a switch statementcanTransitiontakes the current and next statuses as arguments and returns a boolean- Return false with an early return when the status is completed or cancelled
Answer and explanation
// The status type definition (using a union type)
type TaskStatus = 'pending' | 'in_progress' | 'completed' | 'cancelled';
// A function that returns a message based on the status
function getStatusMessage(status: TaskStatus): string {
switch (status) {
case 'pending':
return 'The task has not been started';
case 'in_progress':
return 'The task is in progress';
case 'completed':
return 'The task has been completed';
case 'cancelled':
return 'The task was cancelled';
}
}
// A function that determines whether a status transition is allowed
function canTransition(current: TaskStatus, next: TaskStatus): boolean {
// cannot change once completed or cancelled
if (current === 'completed' || current === 'cancelled') {
return false;
}
// a transition to the same status is not allowed
if (current === next) {
return false;
}
// transitions from pending
if (current === 'pending') {
return next === 'in_progress' || next === 'cancelled';
}
// transitions from in_progress
if (current === 'in_progress') {
return next === 'completed' || next === 'cancelled';
}
return false;
}
// Tests
console.log(getStatusMessage('pending')); // 'The task has not been started'
console.log(getStatusMessage('in_progress')); // 'The task is in progress'
console.log(getStatusMessage('completed')); // 'The task has been completed'
console.log(getStatusMessage('cancelled')); // 'The task was cancelled'
console.log(canTransition('pending', 'in_progress')); // true
console.log(canTransition('in_progress', 'completed')); // true
console.log(canTransition('in_progress', 'cancelled')); // true
console.log(canTransition('completed', 'pending')); // false
console.log(canTransition('cancelled', 'in_progress')); // false
console.log(canTransition('pending', 'completed')); // false (direct completion is not allowed)
Explanation:
- Using a union type lets you strictly define the allowed statuses
- The switch statement in
getStatusMessagehandles every case, so a default clause is unnecessary - The
canTransitionfunction uses the early return pattern to reject invalid transitions first - TypeScript's type checking means a non-existent status (for example
'unknown') cannot be used
An alternative implementation using an enum:
enum TaskStatus {
Pending = 'PENDING',
InProgress = 'IN_PROGRESS',
Completed = 'COMPLETED',
Cancelled = 'CANCELLED'
}
function getStatusMessage(status: TaskStatus): string {
switch (status) {
case TaskStatus.Pending:
return 'The task has not been started';
case TaskStatus.InProgress:
return 'The task is in progress';
case TaskStatus.Completed:
return 'The task has been completed';
case TaskStatus.Cancelled:
return 'The task was cancelled';
}
}
Whether to use a union type or an enum depends on your project's policy and your team's preference. A union type is simple and lightweight, while an enum is easy to use as a namespace.
Summary
What you learned in this chapter:
- Literal types: types that allow only a specific value. You can fix them with as const
- Union types: types that allow one of several types (
A | B | C) - Type narrowing: use typeof and others to narrow a union type to a specific type
- Enums: group related constants. There are numeric enums and string enums
- Modern patterns: the union type + as const combination is often recommended
Key points:
- Combine literal types with union types to strictly limit the allowed values
- Perform exhaustiveness checks with a switch statement
- Understand the characteristics of enums and union types, and choose the right one
In the next chapter, you will learn the basics of functions.
Common pitfalls for beginners
Common mistakes
❌ Forgetting as const, so it does not become a literal type
// ❌ Wrong: without as const, it becomes the string type
const config = {
theme: 'dark',
language: 'ja'
};
// The type of config.theme is string (not 'dark')
function setTheme(theme: 'light' | 'dark'): void {
console.log(theme);
}
// setTheme(config.theme); // Error: a string cannot be assigned to 'light' | 'dark'
// ✅ Correct: fix it as a literal type with as const
const config2 = {
theme: 'dark',
language: 'ja'
} as const;
// The type of config2.theme is 'dark'
setTheme(config2.theme); // OK
Cause: Object properties are inferred as broad types (string, number, and so on) by default.
Solution: Use as const to fix them as literal types.
❌ Trying to operate on a union-type variable as is
// ❌ Wrong: operating without narrowing the type
function processValue(value: string | number): string {
return value.toUpperCase(); // Error: number has no toUpperCase
}
// ✅ Correct: narrow with a type guard before operating
function processValue(value: string | number): string {
if (typeof value === 'string') {
return value.toUpperCase();
}
return value.toString();
}
Cause: On a union-type variable, you can only call methods common to each type directly.
Solution: Narrow the type with typeof or instanceof before operating on it.
❌ Comparing an enum's number directly
enum Status {
Pending, // 0
Active, // 1
Completed // 2
}
// ❌ Wrong: comparing with a magic number
function checkStatus(status: Status): void {
if (status === 1) { // works, but the readability is poor
console.log('Active');
}
}
// ✅ Correct: compare with the enum value
function checkStatus(status: Status): void {
if (status === Status.Active) {
console.log('Active');
}
}
Cause: A numeric enum can be compared with a number, but the intent becomes unclear.
Solution: Always compare using the enum value to make the intent of the code clear.
❌ Trying to reverse-map a string enum
enum Color {
Red = 'RED',
Green = 'GREEN',
Blue = 'BLUE'
}
// ❌ Wrong: a string enum cannot be reverse-mapped
console.log(Color['RED']); // Error: Property 'RED' does not exist (you are accessing it with the value 'RED' rather than the member name 'Red', so it is a type error; undefined when run as JS)
// A numeric enum can be reverse-mapped
enum Direction {
North, // 0
South // 1
}
console.log(Direction[0]); // 'North'
Cause: A string enum does not generate a reverse-mapping table at compile time.
Solution: Do not expect reverse mapping with a string enum; create a separate object if you need it.
❌ Confusing union types and enums
// Using an enum
enum SizeEnum {
Small = 'SMALL',
Medium = 'MEDIUM',
Large = 'LARGE'
}
// Using a union type
type SizeUnion = 'SMALL' | 'MEDIUM' | 'LARGE';
// ❌ Wrong: confusing the values of an enum and a union type
let size1: SizeEnum = 'SMALL'; // Error: a string cannot be assigned directly
let size2: SizeEnum = SizeEnum.Small; // OK
let size3: SizeUnion = 'SMALL'; // OK (a union type can be assigned directly)
Cause: An enum is its own type and is not compatible with string literals.
Solution: When you use an enum, access it in the Enum.Value form.