TypeScript: TypeScript Union Types and Literal Types
Last updated: 2026-08-26
Union types and literal types are the first step in TypeScript's advancement over JavaScript—they allow you to express that "this value can be either A or B" and that "this value can only be one of these options."
1. Union Types
A union type uses | to combine multiple types, indicating that "the value can be any one of these types":
(1) Basic Syntax
let id: number | string;
id = 42; // ✅ number Legal
id = "ABC"; // ✅ string Legal
id = true; // ❌ boolean Not in a composite type
(2) Common Uses: Function Parameters
The most common use of union types is to allow functions to accept parameters of various types:
function printId(id: number | string) {
console.log("YourID: :" + id);
}
printId(42); // ✅
printId("ABC"); // ✅
printId(true); // ❌
(3) Restrictions on Union Types
Variables of a union type can only access members common to all types:
function process(value: number | string) {
// value.toString() // ✅ number and string All of them toString
// value.toUpperCase() // ❌ number None toUpperCase
// value.toFixed(2) // ❌ string None toFixed
}
value is any union member. Only properties and methods shared by all members can be called directly. To access a member of a specific type, you must first use "type narrowing" (see Section 3).
2. Literal Types
Literal types restrict a variable's value to specific literals—not "any string," but "only these specific strings."
(1) Three Types of Literals
| Literal Types | Syntax | Example |
|---|---|---|
| String Literal | "value1" | "value2" |
"north" | "south" | "east" | "west" |
| Numeric Literals | 1 | 2 | 3 |
0 | 1 | 2 |
| Boolean literal | true | false |
Actually equals boolean (only two values) |
(2) String Literal Types
let direction: "north" | "south" | "east" | "west";
direction = "north"; // ✅
direction = "up"; // ❌ "up" Not in a composite type
(3) Numeric Literal Types
type HttpStatusCode = 200 | 301 | 404 | 500;
let code: HttpStatusCode = 200; // ✅
let code2: HttpStatusCode = 201; // ❌ 201 Not in a composite type
(4) Boolean Literal Types
type YesNo = true | false; // equivalent to boolean
type StrictTrue = true; // It can only be true
true | false is simply a boolean), but it's useful when combined with other types in union types: string | true represents "a string or true."
▶ Example: Defining Role Permissions Using Literal Types
type Role = "admin" | "editor" | "viewer";
function checkPermission(role: Role) {
if (role === "admin") {
console.log("Administrator:Full Access");
} else if (role === "editor") {
console.log("Editor:Editable Content");
} else {
console.log("Viewers:Read-only access");
}
}
checkPermission("admin"); // ✅
checkPermission("editor"); // ✅
checkPermission("guest"); // ❌ Compilation Error——"guest" Not legal Role
Output:
Administrator:Full Access
Editor:Editable Content
3. typenarrowing(Type Narrowing)
Variables of a union type cannot directly access members of a specific type, but the scope can be narrowed through "type narrowing."
(1) typeof narrowing
function process(value: number | string) {
if (typeof value === "string") {
// In this branch,TypeScript knows value is string
console.log(value.toUpperCase()); // ✅ string Methods
} else {
// In this branch,TypeScript knows value is number
console.log(value.toFixed(2)); // ✅ number Methods
}
}
(2) Narrowing of Equality
function compare(a: string | number, b: string | boolean) {
if (a === b) {
// a and b equal,Their intersection type is string
console.log(a.toUpperCase()); // ✅ Here a It must be string
}
}
(3) Narrowing of the in operator
function process(input: { name: string } | { age: number }) {
if ("name" in input) {
console.log(input.name); // ✅ Has name property,Inferred as { name: string }
} else {
console.log(input.age); // ✅ None name,Inferred as { age: number }
}
}
(4) instanceof narrowing
function processDate(value: Date | string) {
if (value instanceof Date) {
console.log(value.toISOString()); // ✅ Date Methods
} else {
console.log(value.toUpperCase()); // ✅ string Methods
}
}
▶ Example: A Complete Demonstration of Type Narrowing
function formatValue(value: number | string | boolean): string {
if (typeof value === "number") {
return "Numbers:" + value.toFixed(2);
} else if (typeof value === "string") {
return "String:" + value.toUpperCase();
} else {
return "Boolean:" + value;
}
}
console.log(formatValue(3.14159));
console.log(formatValue("hello"));
console.log(formatValue(true));
Output:
Numbers:3.14
String:HELLO
Boolean:true
4. Classic Patterns for Union Types and Literal Types
Combining union types and literal types can enable very powerful type constraints. Here are three classic patterns:
(1) Pattern 1: Discriminated Union
Distinguish union members using different literal values for the same property:
interface Circle {
kind: "circle"; // Identifiable Attributes
radius: number;
}
interface Rectangle {
kind: "rectangle"; // Identifiable Attributes
width: number;
height: number;
}
type Shape = Circle | Rectangle;
function getArea(shape: Shape): number {
if (shape.kind === "circle") {
return Math.PI * shape.radius ** 2; // ✅ Accessible radius
} else {
return shape.width * shape.height; // ✅ Accessible width、height
}
}
kind property, but its value is a different literal type. TypeScript uses the value of kind to narrow the type and safely access the properties specific to that interface.
(2) Pattern 2: Optional values + undefined
type Result = string | undefined;
function search(query: string): Result {
if (query.length === 0) return undefined;
return "Found:" + query;
}
let result = search("TypeScript");
if (result !== undefined) {
console.log(result.toUpperCase()); // ✅ narrowed to string
}
(3) Pattern 3: State Machine
type RequestStatus = "idle" | "loading" | "success" | "error";
interface RequestState {
status: RequestStatus;
data?: string;
error?: string;
}
function renderState(state: RequestState): string {
switch (state.status) {
case "idle":
return "Waiting......";
case "loading":
return "Loading......";
case "success":
return "Success:" + state.data; // ✅ success in that state data Existence
case "error":
return "Error:" + state.error; // ✅ error in that state error Existence
}
}
5. The never Type and Exhaustive Checks
When TypeScript narrows down to an "impossible scenario," the type becomes never:
type Shape = "circle" | "square";
function getIcon(shape: Shape): string {
switch (shape) {
case "circle": return "⭕";
case "square": return "⬜";
default:
// If all case It's all taken care of.,shape Here, the type is never
const _exhaustiveCheck: never = shape; // ✅ If omitted case,An error will occur here
return _exhaustiveCheck;
}
}
never's exhaustive checking pattern—assign a value to a never-typed variable in the switch statement's default branch. If you add a new member to Shape in the future but forget to add a corresponding case, TypeScript will throw an error in the default branch, prompting you to add it.
▶ Example: The Practical Effect of Exhaustive Checking
type TrafficLight = "red" | "yellow" | "green";
function getAction(light: TrafficLight): string {
switch (light) {
case "red": return "Stop";
case "yellow": return "Note";
// Intentional omission "green" 's case
default:
// If you add the next line,TypeScript It will throw an error:
// You cannot convert the type "green" Assignment to a Type "never"
const exhaustive: never = light;
return "Unknown";
}
}
Output:
Error: Type '"green"' is not assignable to type 'never'.
❓ FAQ
typeof detect all types?typeof can only identify the following types: "number," "string," "boolean," "symbol," "bigint," "object," "function," and "undefined." For reference types such as null (typeof null returns "object"), arrays, and Date, you need to use instanceof or other methods to narrow the type. See Lesson 17 for a detailed explanation.never type used?never type primarily appears in two scenarios: (1) It appears automatically when a type is narrowed to an impossible branch, and (2) when a function will never return a value (such as when an exception is thrown or an infinite loop occurs). Beginners only need to understand the "exhaustive check" pattern—using never in the switch’s default clause to check for any missing cases.📖 Summary
- A union type uses
|to combine multiple types, indicating that the value can be any one of them - Literal types restrict values to specific literals (strings, numbers, booleans) and, when combined with union types, serve as enumerations.
- Variables of union types can only access properties shared by all members; they must be "narrowed" using
typeof,in, orinstanceofto access specific members. - A discriminated union is a classic pattern combining a union and a literal—distinguishing types based on the values of shared attributes.
- The
nevertype is used for exhaustive checking to ensure that aswitchstatement covers all possibilities.
📝 Exercises
- Basic Problem (Difficulty ⭐): Define a union type
Status = "active" | "inactive" | "banned"and write a functiongetStatusText(status: Status)that returns the corresponding Chinese description. - Advanced Problem (Difficulty ⭐⭐): Define a distinguishable union type
Result = SuccessResult | ErrorResult, whereSuccessResultconsists ofsuccess: trueanddata: string, andErrorResultconsists ofsuccess: falseanderror: string. Write a function that processesResultand, based on the value ofsuccess, safely accesses eitherdataorerror. - Challenge Problem (Difficulty ⭐⭐⭐): Write a function
describeDay(day: "mon" | "tue" | "wed" | "thu" | "fri" | "sat" | "sun")using exhaustive checking to handle all 7 days in a switch statement. Then, intentionally omit one day and observe the error message generated by the “never” exhaustive check.