Falytom logoFALYTOM
Games
ToolsUI ComponentsServicesProductsAbout
LoginSign Up
menu
  1. Home
  2. /Web Development
  3. /Typescript
  4. /Literal Types
Your Progress0%
0 of 30 completed

TypeScript Topics

Getting Started

  • What is TypeScript?
  • TypeScript vs JavaScript
  • Setting Up TypeScript
  • Your First TypeScript Program

Basic Types

  • Primitive Types
  • Arrays and Tuples
  • Objects and Type Aliases
  • Enums
  • Any, Unknown, and Never

Functions & Interfaces

  • Function Types and Signatures
  • Interfaces
  • Optional and Default Parameters
  • Function Overloading

Advanced Types

  • Union and Intersection Types
  • Type Guards and Narrowing
  • Literal Types
  • Type Assertions and Casting

Generics

  • Introduction to Generics
  • Generic Constraints
  • Utility Types

Classes & OOP

  • Classes and Constructors
  • Access Modifiers
  • Inheritance and Polymorphism
  • Abstract Classes and Interfaces

Practical TypeScript

  • Working with Modules
  • TypeScript with React
  • Async/Await and Promises
  • Working with APIs and JSON
  • Type Declarations and DefinitelyTyped
  • TypeScript Best Practices

Literal Types and Type Narrowing

Precise types with specific values

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

string-literals-basic.ts
// 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

string-literals-aliases.ts
// 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-literals.ts
// 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-literals.ts
// 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-literals-basic.ts
// 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

template-literals-advanced.ts
// 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.

const-assertions.ts
// 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-unions-literals.ts
// 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

control-flow-narrowing.ts
// 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.ts
// 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-management.ts
// 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.ts
// 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-client-routes.ts
// 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 valid

Test 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

Code with 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.

Corrected Code
// 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 TypeScript

Solution: Use only the allowed literal values

Best Practices

  1. Use literal types for fixed sets of values
  2. Prefer string literals over enums for simple cases
  3. Use const assertions for readonly literal types
  4. Create type aliases for reusable literal unions
  5. Use discriminated unions with literal types
  6. Implement exhaustive checking with never type
  7. Use template literal types for string patterns
  8. Document literal types with comments
  9. Leverage control flow for automatic narrowing
  10. Keep literal unions manageable - not too many values
best-practices-example.ts
// ✅ 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 never ensures 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!

Mastering TypeScript literal types! Check this out!

Previous
Type Guards and Narrowing
Next
Type Assertions and Casting

Never Miss a New TypeScript Tutorial

Join 1,000+ developers learning TypeScript step-by-step. Get new tutorials, type safety tips, and exclusive resources delivered to your inbox - completely FREE.

You might also like

UI Components

Ready-made components for your projects

Developer Tools

Boost productivity with handy generators

HTML Game

Learn HTML concepts through interactive gameplay

Join Our Web Development Training Program

Master frontend & backend development with hands-on projects.

TypeScript Tutorials

0 of 30 completed

Your Progress0%

Topics

Getting Started

  • What is TypeScript?
  • TypeScript vs JavaScript
  • Setting Up TypeScript
  • Your First TypeScript Program

Basic Types

  • Primitive Types
  • Arrays and Tuples
  • Objects and Type Aliases
  • Enums
  • Any, Unknown, and Never

Functions & Interfaces

  • Function Types and Signatures
  • Interfaces
  • Optional and Default Parameters
  • Function Overloading

Advanced Types

  • Union and Intersection Types
  • Type Guards and Narrowing
  • Literal Types
  • Type Assertions and Casting

Generics

  • Introduction to Generics
  • Generic Constraints
  • Utility Types

Classes & OOP

  • Classes and Constructors
  • Access Modifiers
  • Inheritance and Polymorphism
  • Abstract Classes and Interfaces

Practical TypeScript

  • Working with Modules
  • TypeScript with React
  • Async/Await and Promises
  • Working with APIs and JSON
  • Type Declarations and DefinitelyTyped
  • TypeScript Best Practices
Falytom logoFALYTOM

Learn technologies like HTML, CSS, JavaScript, React, NodeJS and more through interactive games, tutorials, components and free tools. Transform your businesses with AI chatbots, SEO-optimized websites, and professional development services.

© 2025 Tomilola Group
  • Facebook logo
  • Twitter logo
  • Instagram logo
  • LinkedIn logo
  • GitHub logo
  • YouTube logo