Literal types let you specify exact values a variable can have, not just general types. Instead of saying "this is a string," you can say "this is exactly the string 'pending'." This provides incredible precision and catches bugs at compile time. Combined with type narrowing, literal types enable powerful patterns for modeling your domain. Let's master literal types!
String Literal Types
String literal types specify exact string values. They're perfect for status values, enums, and anything with a fixed set of options.
Basic String Literals
// Single string literal type
let status: "pending";
status = "pending"; // ✅ Works
// status = "approved"; // ❌ Error: not assignable
// Union of string literals
type Status = "pending" | "approved" | "rejected";
let orderStatus: Status = "pending";
orderStatus = "approved"; // ✅ Works
orderStatus = "rejected"; // ✅ Works
// orderStatus = "completed"; // ❌ Error: not in union
// Function with literal parameter
function setStatus(status: "active" | "inactive" | "suspended"): void {
console.log(`Setting status to: ${status}`);
}
setStatus("active"); // ✅ Works
setStatus("inactive"); // ✅ Works
setStatus("suspended"); // ✅ Works
// setStatus("deleted"); // ❌ Error
// Return type as literal
function getEnvironment(): "development" | "staging" | "production" {
return "development";
}
const env = getEnvironment(); // Type: "development" | "staging" | "production"
// Literal types are exact
type Currency = "NGN" | "USD" | "EUR" | "GBP";
function formatPrice(amount: number, currency: Currency): string {
switch (currency) {
case "NGN":
return `₦${amount.toLocaleString()}`;
case "USD":
return `$${amount.toLocaleString()}`;
case "EUR":
return `€${amount.toLocaleString()}`;
case "GBP":
return `£${amount.toLocaleString()}`;
}
}
console.log(formatPrice(50000, "NGN")); // "₦50,000"
console.log(formatPrice(100, "USD")); // "$100"String Literals with Type Aliases
// Reusable string literal types
type UserRole = "admin" | "editor" | "viewer";
type OrderStatus = "pending" | "processing" | "shipped" | "delivered";
type PaymentMethod = "cash" | "card" | "bank_transfer" | "mobile_money";
// Using literal types
interface User {
id: number;
name: string;
role: UserRole;
}
interface Order {
id: number;
status: OrderStatus;
payment: PaymentMethod;
total: number;
}
const user: User = {
id: 1,
name: "Yusuf Ibrahim",
role: "admin"
};
const order: Order = {
id: 1001,
status: "pending",
payment: "mobile_money",
total: 50000
};
// Function handling specific roles
function hasPermission(role: UserRole, action: string): boolean {
if (role === "admin") {
return true; // Admin can do everything
}
if (role === "editor") {
return action === "read" || action === "write";
}
// Viewer can only read
return action === "read";
}
console.log(hasPermission(user.role, "delete")); // true (admin)
// Processing orders by status
function processOrder(order: Order): string {
switch (order.status) {
case "pending":
return "Waiting for payment confirmation";
case "processing":
return "Preparing order for shipment";
case "shipped":
return "Order is on the way";
case "delivered":
return "Order has been delivered";
}
}
console.log(processOrder(order)); // "Waiting for payment confirmation"Numeric Literal Types
Numeric literal types specify exact numbers. They're useful for HTTP status codes, version numbers, or any numeric constants.
// Numeric literal types
type HttpSuccessCode = 200 | 201 | 204;
type HttpClientError = 400 | 401 | 403 | 404;
type HttpServerError = 500 | 502 | 503;
type HttpStatusCode = HttpSuccessCode | HttpClientError | HttpServerError;
// Using numeric literals
interface ApiResponse {
statusCode: HttpStatusCode;
message: string;
data?: any;
}
function handleResponse(response: ApiResponse): void {
if (response.statusCode === 200) {
console.log("Success:", response.data);
} else if (response.statusCode === 404) {
console.error("Not found:", response.message);
} else if (response.statusCode >= 500) {
console.error("Server error:", response.message);
}
}
const successResponse: ApiResponse = {
statusCode: 200,
message: "OK",
data: { user: "Yusuf" }
};
const notFoundResponse: ApiResponse = {
statusCode: 404,
message: "User not found"
};
handleResponse(successResponse);
handleResponse(notFoundResponse);
// Port numbers as literals
type CommonPort = 80 | 443 | 8080 | 3000;
interface ServerConfig {
host: string;
port: CommonPort;
secure: boolean;
}
const config: ServerConfig = {
host: "localhost",
port: 3000,
secure: false
};
// Version numbers
type MajorVersion = 1 | 2 | 3 | 4 | 5;
type MinorVersion = 0 | 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9;
interface Version {
major: MajorVersion;
minor: MinorVersion;
patch: number;
}
const version: Version = {
major: 4,
minor: 5,
patch: 0
};
console.log(`Version: ${version.major}.${version.minor}.${version.patch}`);
// Dice roll (1-6)
type DiceValue = 1 | 2 | 3 | 4 | 5 | 6;
function rollDice(): DiceValue {
return (Math.floor(Math.random() * 6) + 1) as DiceValue;
}
const roll: DiceValue = rollDice();
console.log(`You rolled: ${roll}`);Boolean Literal Types
Boolean literal types specify exact true or false values. While less common than string/numeric literals, they're useful for flags and strict boolean states.
// Boolean literal types
type AlwaysTrue = true;
type AlwaysFalse = false;
// More practical: conditional types
interface EnabledFeature {
enabled: true;
config: {
maxUsers: number;
timeout: number;
};
}
interface DisabledFeature {
enabled: false;
}
type Feature = EnabledFeature | DisabledFeature;
function getFeatureConfig(feature: Feature): void {
if (feature.enabled === true) {
// TypeScript knows feature is EnabledFeature
console.log(`Max users: ${feature.config.maxUsers}`);
console.log(`Timeout: ${feature.config.timeout}`);
} else {
// TypeScript knows feature is DisabledFeature
console.log("Feature is disabled");
}
}
const enabled: EnabledFeature = {
enabled: true,
config: {
maxUsers: 100,
timeout: 30000
}
};
const disabled: DisabledFeature = {
enabled: false
};
getFeatureConfig(enabled);
getFeatureConfig(disabled);
// Strict boolean in function parameters
function processData(
data: string[],
sortAscending: true
): string[];
function processData(
data: string[],
sortAscending: false
): string[];
function processData(
data: string[],
sortAscending: boolean
): string[] {
if (sortAscending === true) {
return [...data].sort();
}
return [...data].sort().reverse();
}
const items = ["banana", "apple", "cherry"];
const ascending = processData(items, true); // ["apple", "banana", "cherry"]
const descending = processData(items, false); // ["cherry", "banana", "apple"]
console.log(ascending);
console.log(descending);Template Literal Types
Template literal types let you create types based on string templates. They're incredibly powerful for creating precise string patterns.
Basic Template Literals
// Template literal types
type Greeting = `Hello ${"World" | "TypeScript" | "Africa"}`;
let greeting1: Greeting = "Hello World";
let greeting2: Greeting = "Hello TypeScript";
let greeting3: Greeting = "Hello Africa";
// let greeting4: Greeting = "Hello Everyone"; // ❌ Error
// Combining literals
type Color = "red" | "green" | "blue";
type Size = "small" | "medium" | "large";
type ColoredSize = `${Color}-${Size}`;
let item1: ColoredSize = "red-small";
let item2: ColoredSize = "blue-large";
let item3: ColoredSize = "green-medium";
// let item4: ColoredSize = "yellow-small"; // ❌ Error
// CSS properties
type CSSUnit = "px" | "em" | "rem" | "%";
type CSSValue = `${number}${CSSUnit}`;
// This is the type, but values need runtime validation
let width: string = "100px";
let height: string = "50%";
// Event names
type EventType = "click" | "focus" | "blur";
type EventName = `on${Capitalize<EventType>}`;
// Type is: "onClick" | "onFocus" | "onBlur"
let handler1: EventName = "onClick";
let handler2: EventName = "onFocus";
let handler3: EventName = "onBlur";
// HTTP methods with paths
type HttpMethod = "GET" | "POST" | "PUT" | "DELETE";
type ApiPath = "/users" | "/products" | "/orders";
type Endpoint = `${HttpMethod} ${ApiPath}`;
let endpoint1: Endpoint = "GET /users";
let endpoint2: Endpoint = "POST /products";
let endpoint3: Endpoint = "DELETE /orders";
console.log(endpoint1); // "GET /users"Advanced Template Literals
// Uppercase/Lowercase transformations
type Role = "admin" | "user" | "guest";
type UpperRole = Uppercase<Role>; // "ADMIN" | "USER" | "GUEST"
type LowerRole = Lowercase<Role>; // "admin" | "user" | "guest"
type CapRole = Capitalize<Role>; // "Admin" | "User" | "Guest"
type UncapRole = Uncapitalize<UpperRole>; // "aDMIN" | "uSER" | "gUEST"
let upper: UpperRole = "ADMIN";
let lower: LowerRole = "admin";
let cap: CapRole = "Admin";
// Property getters/setters
type Property = "name" | "age" | "email";
type Getter = `get${Capitalize<Property>}`;
type Setter = `set${Capitalize<Property>}`;
// Types: "getName" | "getAge" | "getEmail"
// "setName" | "setAge" | "setEmail"
interface UserMethods {
getName(): string;
getAge(): number;
getEmail(): string;
setName(name: string): void;
setAge(age: number): void;
setEmail(email: string): void;
}
// Database table names
type TableName = "users" | "products" | "orders";
type TableAction = "create" | "read" | "update" | "delete";
type Permission = `${TableAction}_${TableName}`;
// Results in all combinations:
// "create_users" | "read_users" | "update_users" | "delete_users"
// "create_products" | "read_products" | ...
// "create_orders" | "read_orders" | ...
let permission1: Permission = "create_users";
let permission2: Permission = "read_products";
let permission3: Permission = "delete_orders";
// Route patterns
type HttpMethod = "get" | "post" | "put" | "delete";
type RouteHandler = `${HttpMethod}${Capitalize<string>}`;
// Examples: "getUser", "postOrder", "putProduct", "deleteItem"
let route1: string = "getUser"; // Would be RouteHandler type
let route2: string = "postOrder"; // Would be RouteHandler type
// Status combinations
type Status = "pending" | "active" | "completed";
type Priority = "low" | "medium" | "high";
type TaskState = `${Status}-${Priority}`;
let task1: TaskState = "pending-high";
let task2: TaskState = "active-medium";
let task3: TaskState = "completed-low";
console.log(`Task: ${task1}`); // "Task: pending-high"Const Assertions
The as const assertion makes TypeScript infer the most specific (literal) type possible, creating readonly literal types.
// Without const assertion
let status1 = "pending"; // Type: string
// With const assertion
let status2 = "pending" as const; // Type: "pending"
// status2 = "approved"; // ❌ Error: can't change literal type
// Object with const assertion
const config = {
host: "localhost",
port: 8080,
secure: false
} as const;
// Type:
// {
// readonly host: "localhost";
// readonly port: 8080;
// readonly secure: false;
// }
// config.host = "example.com"; // ❌ Error: readonly
// config.port = 3000; // ❌ Error: readonly
// Array with const assertion
const colors = ["red", "green", "blue"] as const;
// Type: readonly ["red", "green", "blue"]
// colors.push("yellow"); // ❌ Error: readonly
// colors[0] = "purple"; // ❌ Error: readonly
// Using const assertion for literal types
const routes = {
home: "/",
about: "/about",
contact: "/contact"
} as const;
type Routes = typeof routes;
type RoutePath = Routes[keyof Routes]; // "/" | "/about" | "/contact"
function navigate(path: RoutePath): void {
console.log(`Navigating to: ${path}`);
}
navigate(routes.home); // ✅ Works
navigate(routes.about); // ✅ Works
// navigate("/profile"); // ❌ Error: not in literal union
// Const assertion with nested objects
const appConfig = {
api: {
baseUrl: "https://api.example.com",
timeout: 30000
},
features: {
auth: true,
analytics: false
}
} as const;
// All properties are readonly and have literal types
// appConfig.api.baseUrl = "new-url"; // ❌ Error
// Function parameters with const assertion
const actions = ["create", "read", "update", "delete"] as const;
type Action = typeof actions[number]; // "create" | "read" | "update" | "delete"
function performAction(action: Action): void {
console.log(`Performing action: ${action}`);
}
actions.forEach(action => performAction(action)); // ✅ All valid
// Enum-like pattern with const assertion
const UserRole = {
ADMIN: "admin",
EDITOR: "editor",
VIEWER: "viewer"
} as const;
type UserRoleType = typeof UserRole[keyof typeof UserRole];
// Type: "admin" | "editor" | "viewer"
function checkRole(role: UserRoleType): void {
if (role === UserRole.ADMIN) {
console.log("Administrator access");
}
}
checkRole(UserRole.ADMIN); // ✅ Works
checkRole("admin"); // ✅ Works (same literal type)Advanced Narrowing Patterns
Discriminated Unions with Literals
// Discriminated union with literal types
type SuccessResult = {
status: "success";
data: {
id: number;
name: string;
};
};
type ErrorResult = {
status: "error";
error: {
code: number;
message: string;
};
};
type LoadingResult = {
status: "loading";
progress: number;
};
type Result = SuccessResult | ErrorResult | LoadingResult;
function handleResult(result: Result): void {
switch (result.status) {
case "success":
// TypeScript knows result is SuccessResult
console.log(`Success: ${result.data.name}`);
break;
case "error":
// TypeScript knows result is ErrorResult
console.error(`Error ${result.error.code}: ${result.error.message}`);
break;
case "loading":
// TypeScript knows result is LoadingResult
console.log(`Loading: ${result.progress}%`);
break;
}
}
const success: Result = {
status: "success",
data: { id: 1, name: "Yusuf" }
};
const error: Result = {
status: "error",
error: { code: 404, message: "Not found" }
};
const loading: Result = {
status: "loading",
progress: 50
};
handleResult(success);
handleResult(error);
handleResult(loading);Control Flow Narrowing
// TypeScript tracks types through control flow
type Value = string | number | null;
function processValue(value: Value): void {
// Initial type: string | number | null
if (value === null) {
console.log("Value is null");
return;
}
// After null check, type is: string | number
if (typeof value === "string") {
// Type narrowed to: string
console.log(value.toUpperCase());
return;
}
// Type narrowed to: number
console.log(value.toFixed(2));
}
processValue(null); // "Value is null"
processValue("hello"); // "HELLO"
processValue(123.456); // "123.46"
// Narrowing with multiple conditions
type Status = "pending" | "approved" | "rejected" | "cancelled";
function getStatusMessage(status: Status): string {
if (status === "pending") {
return "Waiting for approval";
}
if (status === "approved" || status === "rejected") {
// Type: "approved" | "rejected"
return `Request ${status}`;
}
// Type narrowed to: "cancelled"
return "Request cancelled";
}
console.log(getStatusMessage("pending")); // "Waiting for approval"
console.log(getStatusMessage("approved")); // "Request approved"
console.log(getStatusMessage("rejected")); // "Request rejected"
console.log(getStatusMessage("cancelled")); // "Request cancelled"
// Narrowing with early returns
function formatCurrency(
amount: number,
currency: "NGN" | "USD" | "EUR"
): string {
if (currency === "NGN") {
return `₦${amount.toLocaleString()}`;
}
if (currency === "USD") {
return `$${amount.toLocaleString()}`;
}
// TypeScript knows currency is "EUR" here
return `€${amount.toLocaleString()}`;
}
console.log(formatCurrency(50000, "NGN")); // "₦50,000"
console.log(formatCurrency(100, "USD")); // "$100"
console.log(formatCurrency(200, "EUR")); // "€200"Exhaustive Checking with never
// Exhaustive checking ensures all cases are handled
type Direction = "north" | "south" | "east" | "west";
function move(direction: Direction): void {
switch (direction) {
case "north":
console.log("Moving north");
break;
case "south":
console.log("Moving south");
break;
case "east":
console.log("Moving east");
break;
case "west":
console.log("Moving west");
break;
default:
// This ensures we handled all cases
const exhaustiveCheck: never = direction;
throw new Error(`Unhandled direction: ${exhaustiveCheck}`);
}
}
// If we add a new direction to the type:
// type Direction = "north" | "south" | "east" | "west" | "northeast";
// TypeScript will error in the default case!
move("north"); // "Moving north"
move("east"); // "Moving east"
// Exhaustive checking with return values
type PaymentStatus = "pending" | "completed" | "failed" | "refunded";
function getStatusColor(status: PaymentStatus): string {
switch (status) {
case "pending":
return "yellow";
case "completed":
return "green";
case "failed":
return "red";
case "refunded":
return "blue";
default:
const exhaustive: never = status;
throw new Error(`Unhandled status: ${exhaustive}`);
}
}
console.log(getStatusColor("pending")); // "yellow"
console.log(getStatusColor("completed")); // "green"
console.log(getStatusColor("failed")); // "red"
console.log(getStatusColor("refunded")); // "blue"Practical Examples
Example 1: Order Management System
// Order status with literal types
type OrderStatus = "pending" | "confirmed" | "shipped" | "delivered" | "cancelled";
type PaymentStatus = "unpaid" | "paid" | "refunded";
type ShippingMethod = "standard" | "express" | "overnight";
interface Order {
id: number;
orderStatus: OrderStatus;
paymentStatus: PaymentStatus;
shippingMethod: ShippingMethod;
total: number;
}
// Status transitions
const validTransitions: Record<OrderStatus, OrderStatus[]> = {
pending: ["confirmed", "cancelled"],
confirmed: ["shipped", "cancelled"],
shipped: ["delivered"],
delivered: [],
cancelled: []
};
function canTransition(
from: OrderStatus,
to: OrderStatus
): boolean {
return validTransitions[from].includes(to);
}
function updateOrderStatus(
order: Order,
newStatus: OrderStatus
): Order | { error: string } {
if (!canTransition(order.orderStatus, newStatus)) {
return {
error: `Cannot transition from ${order.orderStatus} to ${newStatus}`
};
}
return {
...order,
orderStatus: newStatus
};
}
// Calculate shipping cost based on method
function getShippingCost(method: ShippingMethod): number {
switch (method) {
case "standard":
return 1500;
case "express":
return 3000;
case "overnight":
return 5000;
}
}
// Display order information
function displayOrder(order: Order): void {
console.log(`Order #${order.id}`);
console.log(`Status: ${order.orderStatus}`);
console.log(`Payment: ${order.paymentStatus}`);
console.log(`Shipping: ${order.shippingMethod}`);
console.log(`Total: ₦${order.total + getShippingCost(order.shippingMethod)}`);
}
const order: Order = {
id: 1001,
orderStatus: "pending",
paymentStatus: "unpaid",
shippingMethod: "express",
total: 50000
};
displayOrder(order);
const updated = updateOrderStatus(order, "confirmed");
if ("error" in updated) {
console.error(updated.error);
} else {
displayOrder(updated);
}Example 2: Permission System
// Permission system with literal types
type Resource = "users" | "products" | "orders" | "reports";
type Action = "create" | "read" | "update" | "delete";
type Permission = `${Action}:${Resource}`;
type Role = "admin" | "manager" | "staff" | "customer";
// Permission mappings
const rolePermissions: Record<Role, Permission[]> = {
admin: [
"create:users", "read:users", "update:users", "delete:users",
"create:products", "read:products", "update:products", "delete:products",
"create:orders", "read:orders", "update:orders", "delete:orders",
"create:reports", "read:reports", "update:reports", "delete:reports"
],
manager: [
"read:users", "update:users",
"create:products", "read:products", "update:products",
"read:orders", "update:orders",
"create:reports", "read:reports"
],
staff: [
"read:products",
"create:orders", "read:orders", "update:orders"
],
customer: [
"create:orders", "read:orders"
]
};
function hasPermission(
role: Role,
permission: Permission
): boolean {
return rolePermissions[role].includes(permission);
}
function checkAccess(
role: Role,
action: Action,
resource: Resource
): boolean {
const permission: Permission = `${action}:${resource}`;
return hasPermission(role, permission);
}
// Usage
const userRole: Role = "manager";
console.log(checkAccess(userRole, "read", "users")); // true
console.log(checkAccess(userRole, "delete", "users")); // false
console.log(checkAccess(userRole, "create", "products")); // true
console.log(checkAccess(userRole, "delete", "products")); // false
// Get allowed actions for a resource
function getAllowedActions(role: Role, resource: Resource): Action[] {
const permissions = rolePermissions[role];
const actions: Action[] = [];
const resourcePermissions = permissions.filter(p =>
p.endsWith(`:${resource}`)
);
resourcePermissions.forEach(p => {
const action = p.split(":")[0] as Action;
actions.push(action);
});
return actions;
}
console.log(getAllowedActions("manager", "products"));
// ["create", "read", "update"]
console.log(getAllowedActions("customer", "orders"));
// ["create", "read"]Example 3: API Client with Literal Routes
// API routes with literal types
type HttpMethod = "GET" | "POST" | "PUT" | "DELETE";
type ApiVersion = "v1" | "v2";
type Endpoint =
| "/users"
| "/users/:id"
| "/products"
| "/products/:id"
| "/orders"
| "/orders/:id";
type ApiRoute = `${ApiVersion}${Endpoint}`;
// Route configuration
interface RouteConfig {
method: HttpMethod;
route: ApiRoute;
body?: any;
}
// Type-safe API client
class ApiClient {
private baseUrl = "https://api.example.com";
async request<T>(config: RouteConfig): Promise<T> {
const url = `${this.baseUrl}/${config.route}`;
const response = await fetch(url, {
method: config.method,
headers: {
"Content-Type": "application/json"
},
body: config.body ? JSON.stringify(config.body) : undefined
});
return response.json();
}
// Type-safe methods
getUsers() {
return this.request({
method: "GET",
route: "v1/users"
});
}
getUser(id: number) {
return this.request({
method: "GET",
route: "v1/users/:id" // In real implementation, replace :id
});
}
createProduct(product: any) {
return this.request({
method: "POST",
route: "v1/products",
body: product
});
}
updateOrder(id: number, updates: any) {
return this.request({
method: "PUT",
route: "v1/orders/:id", // In real implementation, replace :id
body: updates
});
}
deleteProduct(id: number) {
return this.request({
method: "DELETE",
route: "v1/products/:id" // In real implementation, replace :id
});
}
}
const client = new ApiClient();
// TypeScript ensures correct routes and methods
client.getUsers();
client.createProduct({ name: "Laptop", price: 450000 });
// These would cause type errors with wrong combinations:
// client.request({ method: "GET", route: "v3/users" }); // ❌ v3 not valid
// client.request({ method: "PATCH", route: "v1/users" }); // ❌ PATCH not validTest Your Knowledge
Type Safety Check
Which literal type definition is correct?
// Option A
let status: "pending" | "approved" | "rejected";
status = "pending";
status = "completed";
// Option B
type Status = "pending" | "approved" | "rejected";
let status: Status = "pending";
status = "approved";
// Option C
const status = "pending";
status = "approved";
// Option D
let status: string = "pending";
status = "approved";Common TypeScript Error
// Trying to assign invalid literal
type Direction = "north" | "south" | "east" | "west";
let direction: Direction = "north"; // ✅ Works
direction = "northeast"; // ❌ Error!
// Function expecting specific literal
function move(direction: Direction): void {
console.log(`Moving ${direction}`);
}
move("north"); // ✅ Works
move("up"); // ❌ Error!
move("southwest"); // ❌ Error!❌ Type '"northeast"' is not assignable to type 'Direction'. Type '"up"' is not assignable to parameter of type 'Direction'.
What's Wrong?
Literal types only accept the exact values specified. TypeScript enforces that only the defined literal values can be used, preventing typos and invalid values.
// Fixed: Use valid literal values
type Direction = "north" | "south" | "east" | "west";
let direction: Direction = "north"; // ✅ Works
direction = "south"; // ✅ Works
direction = "east"; // ✅ Works
direction = "west"; // ✅ Works
function move(direction: Direction): void {
console.log(`Moving ${direction}`);
}
move("north"); // ✅ Works
move("south"); // ✅ Works
move("east"); // ✅ Works
move("west"); // ✅ Works
// All valid directions are enforced by TypeScriptSolution: Use only the allowed literal values
Best Practices
- Use literal types for fixed sets of values
- Prefer string literals over enums for simple cases
- Use const assertions for readonly literal types
- Create type aliases for reusable literal unions
- Use discriminated unions with literal types
- Implement exhaustive checking with never type
- Use template literal types for string patterns
- Document literal types with comments
- Leverage control flow for automatic narrowing
- Keep literal unions manageable - not too many values
// ✅ Good: Clear literal types with type alias
type Status = "pending" | "approved" | "rejected";
function processStatus(status: Status): void {
switch (status) {
case "pending":
console.log("Waiting");
break;
case "approved":
console.log("Approved");
break;
case "rejected":
console.log("Rejected");
break;
default:
const exhaustive: never = status;
throw new Error(`Unhandled: ${exhaustive}`);
}
}
// ✅ Good: Const assertion for config
const config = {
apiUrl: "https://api.example.com",
timeout: 30000,
retries: 3
} as const;
// ✅ Good: Template literal for patterns
type EventHandler = `on${Capitalize<string>}`;
// ✅ Good: Discriminated union
type Result =
| { status: "success"; data: any }
| { status: "error"; error: string };
// ❌ Avoid: Too many literals (hard to maintain)
type TooMany =
| "value1" | "value2" | "value3" | "value4" | "value5"
| "value6" | "value7" | "value8" | "value9" | "value10"
| "value11" | "value12" | "value13" | "value14" | "value15";
// Consider enum or different approach
// ❌ Avoid: Magic strings without types
function bad(status: string) { // Too broad
if (status === "pending") {
// Typos not caught!
}
}
// ✅ Better: Use literal type
function good(status: Status) { // Precise
if (status === "pending") {
// Typos caught by TypeScript
}
}Key Takeaways
- Literal types specify exact values, not general types
- String literals are perfect for status values and fixed options
- Numeric literals work well for codes, versions, and constants
- Boolean literals enable strict true/false type checking
- Template literal types create precise string patterns
- Const assertions (
as const) create readonly literal types - TypeScript narrows types automatically through control flow
- Discriminated unions combine literals with type narrowing
- Exhaustive checking with
neverensures completeness - Literal types prevent typos and invalid values at compile-time
What's Next?
Outstanding work! You've mastered literal types and advanced type narrowing patterns. This completes the "Advanced Types" category! Next, we'll dive into Generics, starting with Introduction to Generics. You'll learn how to create reusable, type-safe components and functions that work with any type. Generics are one of TypeScript's most powerful features!
Get ready to unlock the full power of TypeScript generics!