Skip to main content

TS Ep 8: Literal Types — Unit Types & Enum Alternatives

Rachmat Hidayat
Author
Rachmat Hidayat
Learn & sharing insights on TypeScript, Go, Kubernetes, DevOps, DevSecOps, SRE, Platform Engineering, AI/ML Engineering, and MLOps.
typescript - This article is part of a series.
Part 8: This Article
A primitive type like string represents an infinite set of text values. A Literal Type represents an exact, singular unit value (such as "admin" or 404). Combining literal types into unions provides type safety with zero runtime JavaScript overhead.

1. What are Literal Types?
#

In set theory, a type containing exactly one value is called a Unit Type (or Singleton Type). In TypeScript, these are called Literal Types.

// 'mode' cannot hold any string—it can ONLY hold the exact string "dark"
let mode: "dark" = "dark";

// ❌ Compiler Error: Type '"light"' is not assignable to type '"dark"'.
// mode = "light"; 

TypeScript supports four forms of literal types:

  1. String Literals: "GET", "POST", "admin"
  2. Numeric Literals: 200, 404, 500, 3.14
  3. Boolean Literals: true, false (Notice that boolean is just a built-in union alias for true | false!)
  4. BigInt Literals: 100n, 0n

2. Literal Unions (Zero-Overhead Enum Replacement)
#

A single literal type is rarely useful on its own. However, when combined into a Union of Literals, they form one of TypeScript’s most powerful features:

type HttpMethod = "GET" | "POST" | "PUT" | "DELETE" | "PATCH";

function makeRequest(url: string, method: HttpMethod) {
  console.log(`Sending ${method} request to ${url}`);
}

makeRequest("https://api.acme.com/users", "GET"); // 🟢 Valid!

// ❌ Compiler Error: Argument of type '"CONNECT"' is not assignable to parameter of type 'HttpMethod'.
// makeRequest("https://api.acme.com/users", "CONNECT"); 

Why Literal Unions beat Enums in TypeScript
#

FeatureString Literal Unions ("a" | "b")TypeScript Enums (enum Role)
Runtime JS CodeZero bytes (Completely erased)Emits boilerplate JS IIFE objects
JSON SerializationDirect string values ("admin")Requires mapping enum object keys
Structural CompatibilityPass string literal "admin" directlyRequires Role.Admin reference
Bundle ImpactTree-shakes perfectlyLeaves dead enum objects in bundle

3. Numeric Literal Unions for Domain Rules
#

You can use numeric literal unions to enforce strict domain boundaries (such as HTTP status codes, dice rolls, or specific port numbers):

type HttpStatus = 200 | 201 | 400 | 401 | 403 | 404 | 500;

interface ApiResponse {
  status: HttpStatus;
  data: unknown;
}

const res: ApiResponse = {
  status: 200, // Valid
  data: { user: "Alice" },
};

// ❌ Error: Type '202' is not assignable to type 'HttpStatus'.
// res.status = 202; 

4. The Object Property Widening Trap
#

A very common bug occurs when passing an object property containing a string literal to a function expecting a Literal Union.

Consider this scenario:

type HttpMethod = "GET" | "POST";

function fetchUrl(url: string, method: HttpMethod) {
  // ...
}

// Declare configuration object with 'const'
const reqConfig = {
  url: "https://api.example.com",
  method: "GET",
};

// ❌ Compiler Error: Argument of type 'string' is not assignable to parameter of type 'HttpMethod'.
// fetchUrl(reqConfig.url, reqConfig.method);

Why did this happen?
#

Even though reqConfig was declared using const, JavaScript objects are mutable by default. You can change reqConfig.method = "PUT" on the very next line without reassigning reqConfig.

Therefore, TypeScript infers reqConfig.method as broad string rather than literal "GET".

3 Solutions to the Widening Trap
#

Solution 1: Inline Type Annotation on Object
#

Explicitly annotate the object property with the literal union:

const reqConfig: { url: string; method: HttpMethod } = {
  url: "https://api.example.com",
  method: "GET",
};

fetchUrl(reqConfig.url, reqConfig.method); // 🟢 WORKED!

Solution 2: Type Assertion (as const)
#

Assert the literal type on the property value or the entire object:

const reqConfig = {
  url: "https://api.example.com",
  method: "GET" as const, // Locks down method to literal "GET"
};

fetchUrl(reqConfig.url, reqConfig.method); // 🟢 WORKED!

Solution 3: Full Object Immutability (as const)
#

Lock down all properties of the object to literal types and readonly:

const reqConfig = {
  url: "https://api.example.com",
  method: "GET",
} as const; // Inferred type: { readonly url: "https://api.example.com"; readonly method: "GET" }

fetchUrl(reqConfig.url, reqConfig.method); // 🟢 WORKED!

Summary & Next Steps
#

In this episode:

  • We defined unit types (Singletons) like "GET", 404, true.
  • We proved why String Literal Unions are preferred over Enums due to zero runtime footprint and clean JSON serialization.
  • We investigated Object Property Literal Widening and solved it using as const.

In Episode 9: Type Narrowing with typeof and instanceof, we will learn how TypeScript’s control flow analysis narrows broad types down to specific concrete types!

typescript - This article is part of a series.
Part 8: This Article