TypeScript 11 ๐ท Literal Types and Const Assertions
A literal type is a type with exactly one value โ the string 'loading', the number 42, the boolean true. Where string means “any string,” 'loading' means “this exact string and nothing else.” Literal types are how TypeScript models closed sets: allowed values, config keys, status codes, event names. And as const is the operator that produces them โ it tells the compiler “don’t widen this, keep the exact literals.” Together they’re one of TypeScript’s most useful patterns, powering discriminated unions, exhaustive checks, and runtime-safe constants.
Key point: Literal types are narrower versions of string, number, and boolean. They exist at the type level only โ at runtime, a 'loading' value is just the string 'loading'. as const is the assertion that produces literal types from values that would otherwise widen. Every discriminated union, every config constant, every event name list is built on these two features.
What a literal type is
A literal type describes exactly one value.
let a: 'hello' = 'hello'; // only 'hello'
let b: 42 = 42; // only 42
let c: true = true; // only true
a = 'world'; // โ
b = 43; // โ
c = false; // โ
'hello' is not string โ it’s a subtype of string with a single member. 42 is not number โ it’s a subtype of number. true is not boolean โ it’s a subtype of boolean.
The three kinds of literal types:
| Kind | Example | Underlying type |
|---|---|---|
| String literal | 'loading' | string |
| Numeric literal | 42, -1, 3.14 | number |
| Boolean literal | true, false | boolean |
Bigint and symbol literals exist too, but they’re rarer.
Why they’re subtypes:
const x: string = 'hello'; // โ
string literal assignable to string
const y: 'hello' = 'world'; // โ wrong literal
A 'hello' is a string โ every 'hello' is a string, but not every string is 'hello'. That direction matters: literal types fit where general types are expected, not the reverse.
Why literal types matter: They let you say “these exact values” instead of “any string.” That’s the foundation of discriminated unions, exhaustive switches, config schemas, and API contracts. Without literal types,
stringwould be the best you could do โ and the compiler couldn’t catch"loadng"(typo) versus"loading".
How literals arise
Literal types come from three places: const declarations, as const, and explicit annotations.
From const:
const name = 'Alice'; // type: 'Alice' (literal)
const count = 42; // type: 42
const active = true; // type: true
let lname = 'Alice'; // type: string (widened)
let lcount = 42; // type: number
let lactive = true; // type: boolean
const preserves the literal. let widens to the general type.
From explicit annotation:
let status: 'loading' | 'ready' = 'loading';
The annotation forces the literal union.
From as const:
const config = { mode: 'dark' } as const;
// type: { readonly mode: 'dark' }
const colors = ['red', 'green'] as const;
// type: readonly ['red', 'green']
as const widens nothing โ every literal stays literal, and the structure becomes readonly.
From function parameters with literal types:
function setMode(mode: 'light' | 'dark'): void { }
setMode('light'); // โ
setMode('dark'); // โ
setMode('blue'); // โ
From return type annotations:
function getState(): 'idle' | 'active' {
return 'idle';
}
Why const preserves literals: A const binding can’t be reassigned, so the exact value is the type. let can be reassigned to another value of the same general type, so the type is widened. This is deliberate โ it keeps the language ergonomic.
Why
constuses literals by default: For a truly immutable binding, the value is the type. Aconstcannot hold anything else. TypeScript models this faithfully. That meansconst status = 'loading'gives you'loading', notstringโ a small win that compounds in literal unions.
Literal unions โ closed sets
The most common use of literal types is a union of literals to describe a closed set of allowed values.
type Status = 'idle' | 'loading' | 'ready' | 'error';
type Direction = 'north' | 'south' | 'east' | 'west';
type LogLevel = 'debug' | 'info' | 'warn' | 'error';
type HttpMethod = 'GET' | 'POST' | 'PUT' | 'DELETE';
type Size = 'xs' | 'sm' | 'md' | 'lg' | 'xl';
A value of type Status is exactly one of those four strings.
let s: Status = 'idle'; // โ
s = 'loading'; // โ
s = 'pending'; // โ not in the union
s = 'IDLE'; // โ case-sensitive
Why this is powerful: The compiler knows all valid values, so it can check every assignment, catch typos, and reason about exhaustiveness. The set is closed โ you can’t invent new values at runtime and expect them to type-check.
Numeric literal unions:
type DiceRoll = 1 | 2 | 3 | 4 | 5 | 6;
type Bit = 0 | 1;
type HttpStatus = 200 | 201 | 400 | 401 | 404 | 500;
Boolean literal types:
type Yes = true;
type No = false;
type Flag = true | false; // same as boolean
Mixed literal unions:
type Id = 'new' | number;
A literal string or any number.
The alternative to enums: Literal unions are the modern, zero-runtime-cost alternative to TypeScript enums. They compile to nothing and integrate with the type system better.
// Enum โ emits runtime code
enum Status { Idle = 'idle', Ready = 'ready' }
// Literal union โ no runtime code
type Status = 'idle' | 'ready';
Both express the same set. Literal unions are lighter.
Why literal unions beat enums for most cases: They’re erased at compile time โ no runtime object, no bundle bloat. They work better with
isolatedModules. They integrate cleanly with narrowing and exhaustiveness. Enums still have uses (numeric codes, iterating over values), but for most new code, literal unions are the better choice.
as const โ the const assertion
as const is a const assertion. It tells TypeScript to preserve literal types and make the structure readonly.
On a string:
const a = 'hello'; // 'hello'
const b = 'hello' as const; // 'hello' โ same, already const
let c = 'hello'; // string
let d = 'hello' as const; // 'hello'
as const on a literal is a no-op at the type level for const. It matters on let and on composite values.
On a number:
const n = 42 as const; // 42
On an array:
const colors = ['red', 'green', 'blue'] as const;
// type: readonly ['red', 'green', 'blue']
colors.push('yellow'); // โ readonly
colors[0] = 'purple'; // โ
The array becomes a readonly tuple of literal types. Each element keeps its exact string.
On an object:
const config = {
mode: 'dark',
retries: 3,
features: ['auth', 'logging']
} as const;
// type: {
// readonly mode: 'dark';
// readonly retries: 3;
// readonly features: readonly ['auth', 'logging'];
// }
Every property becomes readonly, and every literal stays literal. Nested objects and arrays get the same treatment recursively.
On an object with variables:
let name = 'Alice';
const obj = { name } as const;
// type: { readonly name: string }
name was string โ as const doesn’t turn string into a literal, because the variable’s type was already string. It only preserves what was already literal.
What as const does:
- Makes the type narrow (literals stay literal)
- Makes the structure readonly (properties and arrays can’t be reassigned)
- Applies recursively to nested objects and arrays
What as const doesn’t do:
- It doesn’t change runtime behavior โ the value is the same object
- It doesn’t turn a widened variable into a literal
- It doesn’t validate anything โ it’s an assertion
Why
as constexists: Object and array literals widen by default.{ mode: 'dark' }becomes{ mode: string }, not{ mode: 'dark' }. For most code, that’s fine โ you’ll mutate the object. For constants, you want the narrow type.as constopts into the narrow reading without changing anything at runtime.
as const on objects โ the config pattern
as const is standard for configuration objects, event name lists, and constant tables.
const CONFIG = {
apiUrl: 'https://api.example.com',
timeout: 5000,
retries: 3,
features: {
auth: true,
logging: false
}
} as const;
Every field is readonly and literal-typed. Nothing can be reassigned, and the exact values are known.
Deriving types from as const:
const CONFIG = {
apiUrl: 'https://api.example.com',
timeout: 5000
} as const;
type Config = typeof CONFIG;
// { readonly apiUrl: 'https://api.example.com'; readonly timeout: 5000 }
Extracting value unions:
const COLORS = ['red', 'green', 'blue'] as const;
type Color = typeof COLORS[number];
// 'red' | 'green' | 'blue'
typeof COLORS[number] โ the element type of the array โ is the union of every literal in it.
Extracting key unions:
const CONFIG = {
dark: 'dark',
light: 'light'
} as const;
type ConfigKey = keyof typeof CONFIG;
// 'dark' | 'light'
type ConfigValue = typeof CONFIG[keyof typeof CONFIG];
// 'dark' | 'light'
Two common patterns:
keyof typeof Xโ union of keystypeof X[keyof typeof X]โ union of values
These extract union types from constant objects without duplicating the values.
Why this pattern is idiomatic: The single source of truth is the runtime object. The types are derived from it. Add a key to CONFIG, and the union grows automatically. No duplication โ one place to change.
const STATUS = {
Idle: 'idle',
Loading: 'loading',
Ready: 'ready'
} as const;
type Status = typeof STATUS[keyof typeof STATUS];
// 'idle' | 'loading' | 'ready'
function setStatus(s: Status): void { }
setStatus(STATUS.Idle); // โ
setStatus('idle'); // โ
setStatus('pending'); // โ
Why derive from the object: If you wrote
type Status = 'idle' | 'loading' | 'ready'and a separate object, the two could drift. One has'loading', the other'loadng'. Deriving from one source of truth prevents that. It also means the object provides runtime constants (for code that needs a value) and the type provides compile-time checking.
Literal types in function signatures
Literal unions are common in function parameters โ they document the allowed values.
function log(message: string, level: 'info' | 'warn' | 'error'): void {
console.log(`[${level.toUpperCase()}] ${message}`);
}
log('started', 'info'); // โ
log('failed', 'error'); // โ
log('hmm', 'debug'); // โ
The parameter type restricts the allowed strings.
Return type as a literal:
function isReady(): true {
return true;
}
Rare, but useful for discriminated unions and type narrowing.
Overloaded literal returns:
function parse(input: 'json'): object;
function parse(input: 'text'): string;
function parse(input: 'json' | 'text'): object | string {
return input === 'json' ? {} : '';
}
The return type depends on the literal argument.
Literal union for configuration:
interface Config {
mode: 'development' | 'production' | 'test';
logLevel: 'debug' | 'info' | 'warn' | 'error';
target: 'es2018' | 'es2020' | 'es2022';
}
const config: Config = {
mode: 'development',
logLevel: 'debug',
target: 'es2022'
};
Each field is a closed set. The compiler catches typos and unlisted values.
Why use literal unions in parameters: They document the API contract directly in the type.
function log(level: string)accepts any string โ including typos.function log(level: 'info' | 'warn' | 'error')accepts exactly three. The second is self-documenting and checked. Autocomplete shows the valid options.
Exhaustiveness with literal unions
Literal unions pair naturally with switch and a never check to enforce exhaustiveness.
type Status = 'idle' | 'loading' | 'ready' | 'error';
function assertNever(x: never): never {
throw new Error(`Unhandled: ${x}`);
}
function message(status: Status): string {
switch (status) {
case 'idle': return 'Waiting';
case 'loading': return 'Loading...';
case 'ready': return 'Ready';
case 'error': return 'Failed';
default: return assertNever(status);
}
}
In the default branch, TypeScript has narrowed status to never โ meaning every case is handled. If you add a new status and forget to handle it, the default branch receives a non-never type, and assertNever fails to compile.
Adding a new variant triggers the error:
type Status = 'idle' | 'loading' | 'ready' | 'error' | 'cancelled';
// Now message() fails because 'cancelled' isn't handled.
// Error: Argument of type 'string' is not assignable to 'never'.
That’s the exhaustive-check pattern. Add a value to the union, and every switch that needs to handle it refuses to compile until updated.
What never means here: never is the empty type โ no value belongs to it. After handling all cases of a union, TypeScript narrows the remaining type to never. That’s how it detects exhaustiveness.
Why this pattern matters: It makes adding a variant a compile-time event. You can’t forget to handle a new case โ the compiler tells you. For state machines, event handlers, and command dispatchers, this is essential.
type Command =
| { kind: 'add'; item: string }
| { kind: 'remove'; id: number }
| { kind: 'clear' };
function execute(cmd: Command): void {
switch (cmd.kind) {
case 'add': return console.log('add', cmd.item);
case 'remove': return console.log('remove', cmd.id);
case 'clear': return console.log('clear');
default: return assertNever(cmd);
}
}
The default branch handles any new command added later โ but only by failing to compile until the case is added.
Why exhaustive checks are powerful: They turn the compiler into a checklist. Every time you extend a union, the compiler forces you to update every consumer. Without them, a missing case becomes a silent runtime bug โ a missing handler, a broken state. With them, the compiler catches it before you ship.
Literal types + discriminated unions
The most common production use of literal types is the discriminated union โ a union of object types with a shared literal field that distinguishes them.
type Success = { status: 'success'; data: string };
type Failure = { status: 'failure'; error: string };
type Loading = { status: 'loading' };
type Result = Success | Failure | Loading;
function display(r: Result): string {
switch (r.status) {
case 'success': return r.data; // r narrowed to Success
case 'failure': return r.error; // r narrowed to Failure
case 'loading': return 'Loading...'; // r narrowed to Loading
}
}
The status field is the discriminant โ a literal type that tells which branch you have. Narrowing on it narrows the whole object.
Why literal types are required here: Without them, status: string wouldn’t narrow. TypeScript needs literal types to distinguish branches. That’s why discriminated unions are impossible without literal types.
With as const for constants:
const SUCCESS = { status: 'success' } as const;
// { readonly status: 'success' }
The literal type is preserved, so the constant is assignable to the discriminated union.
Why this is the killer app of literal types: Discriminated unions model alternatives โ success or failure, one event or another, one shape or another. TypeScript narrows on the discriminant and gives you the right branch. This pattern is everywhere in real code: Redux actions, API results, parser states, command messages.
Why discriminated unions need literals: Without a literal discriminant, there’s nothing to narrow on.
status: stringmatches every branch โ no narrowing.status: 'success'matches exactly one branch, so narrowing works. That’s why literal types aren’t just convenient โ they’re foundational for one of TypeScript’s most important patterns.
A full example
A small state machine that combines literal unions, as const, and exhaustive checks.
// ============================================
// CONSTANT TABLE โ SOURCE OF TRUTH
// ============================================
const EVENTS = {
Load: 'load',
Success: 'success',
Fail: 'fail',
Reset: 'reset'
} as const;
type Event = typeof EVENTS[keyof typeof EVENTS];
// 'load' | 'success' | 'fail' | 'reset'
// ============================================
// STATE TYPE โ LITERAL UNION
// ============================================
type State = 'idle' | 'loading' | 'ready' | 'error';
// ============================================
// TRANSITIONS โ DISCRIMINATED BY EVENT
// ============================================
function transition(state: State, event: Event): State {
switch (state) {
case 'idle':
if (event === EVENTS.Load) return 'loading';
return state;
case 'loading':
if (event === EVENTS.Success) return 'ready';
if (event === EVENTS.Fail) return 'error';
return state;
case 'ready':
if (event === EVENTS.Reset) return 'idle';
return state;
case 'error':
if (event === EVENTS.Reset) return 'idle';
return state;
default:
return assertNever(state);
}
}
function assertNever(x: never): never {
throw new Error(`Unhandled: ${x}`);
}
// ============================================
// USAGE
// ============================================
let state: State = 'idle';
state = transition(state, EVENTS.Load);
console.log(state); // 'loading'
state = transition(state, EVENTS.Success);
console.log(state); // 'ready'
state = transition(state, EVENTS.Reset);
console.log(state); // 'idle'
console.log(EVENTS.Load); // 'load'
Every piece uses literal types:
EVENTSโas constobject producing literal typesEventโ union extracted viatypeof EVENTS[keyof typeof EVENTS]Stateโ literal union of four stringstransitionโ switches on literals, exhaustive viaassertNever- Runtime constants โ
EVENTS.Loadgives'load'
The compiler catches: typos in state names, unhandled states, invalid event strings.
Why this pattern matters: It’s how you model real systems โ state machines, workflows, protocols. The types are the specification, and the compiler enforces them. Adding a new event or state triggers compile errors in every place that needs updating, so you can’t forget.
Complete Example Session
# ============================================
# PART 1: BASIC LITERAL TYPES
# ============================================
cat > literals.ts << 'EOF'
let a: 'hello' = 'hello';
let b: 42 = 42;
let c: true = true;
// a = 'world'; // โ
// b = 43; // โ
// c = false; // โ
const name = 'Alice'; // 'Alice'
let lname = 'Alice'; // string
type Status = 'idle' | 'loading' | 'ready' | 'error';
let s: Status = 'idle';
// s = 'pending'; // โ
console.log(a, b, c, name, lname, s);
EOF
npx tsc --noEmit literals.ts
# (no errors)
# ============================================
# PART 2: `as const` ON OBJECTS
# ============================================
cat > as-const.ts << 'EOF'
const config = {
mode: 'dark',
retries: 3,
features: ['auth', 'logging']
} as const;
// config.mode = 'light'; // โ readonly
// config.features.push('x'); // โ readonly
console.log(config);
EOF
npx tsc --noEmit as-const.ts
# (no errors)
# ============================================
# PART 3: EXTRACTING UNIONS
# ============================================
cat > extract.ts << 'EOF'
const COLORS = ['red', 'green', 'blue'] as const;
type Color = typeof COLORS[number];
// 'red' | 'green' | 'blue'
const STATUS = {
Idle: 'idle',
Ready: 'ready'
} as const;
type StatusKey = keyof typeof STATUS; // 'Idle' | 'Ready'
type StatusValue = typeof STATUS[keyof typeof STATUS]; // 'idle' | 'ready'
function paint(c: Color): void { console.log(c); }
paint('red');
// paint('purple'); // โ
console.log(STATUS.Idle, STATUS.Ready);
EOF
npx tsc --noEmit extract.ts
# (no errors)
# ============================================
# PART 4: EXHAUSTIVE CHECKS
# ============================================
cat > exhaustive.ts << 'EOF'
type Status = 'idle' | 'loading' | 'ready' | 'error';
function assertNever(x: never): never {
throw new Error(`Unhandled: ${x}`);
}
function message(s: Status): string {
switch (s) {
case 'idle': return 'Waiting';
case 'loading': return 'Loading...';
case 'ready': return 'Ready';
case 'error': return 'Failed';
default: return assertNever(s);
}
}
console.log(message('idle'));
console.log(message('ready'));
EOF
npx tsc --noEmit exhaustive.ts
# (no errors)
# ============================================
# PART 5: DISCRIMINATED UNION
# ============================================
cat > discriminated.ts << 'EOF'
type Result =
| { status: 'success'; data: string }
| { status: 'failure'; error: string }
| { status: 'loading' };
function display(r: Result): string {
switch (r.status) {
case 'success': return r.data;
case 'failure': return r.error;
case 'loading': return 'Loading...';
}
}
console.log(display({ status: 'success', data: 'ok' }));
console.log(display({ status: 'loading' }));
EOF
npx tsc --noEmit discriminated.ts
# (no errors)
# ============================================
# PART 6: FULL EXAMPLE
# ============================================
cat > machine.ts << 'EOF'
const EVENTS = {
Load: 'load',
Success: 'success',
Fail: 'fail',
Reset: 'reset'
} as const;
type Event = typeof EVENTS[keyof typeof EVENTS];
type State = 'idle' | 'loading' | 'ready' | 'error';
function assertNever(x: never): never {
throw new Error(`Unhandled: ${x}`);
}
function transition(state: State, event: Event): State {
switch (state) {
case 'idle':
return event === EVENTS.Load ? 'loading' : state;
case 'loading':
if (event === EVENTS.Success) return 'ready';
if (event === EVENTS.Fail) return 'error';
return state;
case 'ready':
return event === EVENTS.Reset ? 'idle' : state;
case 'error':
return event === EVENTS.Reset ? 'idle' : state;
default:
return assertNever(state);
}
}
let state: State = 'idle';
state = transition(state, EVENTS.Load);
console.log(state); // loading
state = transition(state, EVENTS.Success);
console.log(state); // ready
state = transition(state, EVENTS.Reset);
console.log(state); // idle
EOF
npx tsc --noEmit machine.ts
# (no errors)
# ============================================
# PART 7: COMPILE AND RUN
# ============================================
npx tsc literals.ts as-const.ts extract.ts exhaustive.ts discriminated.ts machine.ts
node literals.js
# [ hello 42 true Alice Alice idle ]
node as-const.js
# [ { mode: 'dark', retries: 3, features: [ 'auth', 'logging' ] } ]
node extract.js
# [ red ]
# [ idle ready ]
node exhaustive.js
# [ Waiting ]
# [ Ready ]
node discriminated.js
# [ ok ]
# [ Loading... ]
node machine.js
# [ loading ]
# [ ready ]
# [ idle ]
Quick Reference
Literal Type Syntax
| Kind | Example |
|---|---|
| String literal | 'loading' |
| Number literal | 42 |
| Boolean literal | true |
| Bigint literal | 10n |
| Union | 'a' | 'b' |
How Literals Arise
| Source | Example | Result |
|---|---|---|
const | const x = 'a' | 'a' |
let | let x = 'a' | string |
| Annotation | let x: 'a' | 'b' | literal union |
as const | ['a'] as const | readonly ['a'] |
| Function param | f(m: 'a' | 'b') | literal union |
Widening
| Declaration | Type |
|---|---|
let x = 'a' | string |
const x = 'a' | 'a' |
let x = ['a'] | string[] |
const x = ['a'] | string[] |
const x = ['a'] as const | readonly ['a'] |
const x = { a: 'b' } | { a: string } |
const x = { a: 'b' } as const | { readonly a: 'b' } |
as const Effects
| Input | Output |
|---|---|
'a' | 'a' |
['a', 'b'] | readonly ['a', 'b'] |
{ a: 'b' } | { readonly a: 'b' } |
| Nested objects | Recursively readonly + literals |
string variable | string (no change) |
Union Extraction
| Pattern | Result |
|---|---|
typeof COLORS[number] | Element union |
keyof typeof OBJ | Key union |
typeof OBJ[keyof typeof OBJ] | Value union |
Common Literal Unions
| Type | Values |
|---|---|
| Status | 'idle' | 'loading' | 'ready' | 'error' |
| Direction | 'north' | 'south' | 'east' | 'west' |
| Log level | 'debug' | 'info' | 'warn' | 'error' |
| HTTP method | 'GET' | 'POST' | 'PUT' | 'DELETE' |
| Size | 'xs' | 'sm' | 'md' | 'lg' | 'xl' |
Exhaustive Check Pattern
| Step | Code |
|---|---|
| 1. Helper | function assertNever(x: never): never { throw x; } |
| 2. Switch all cases | case 'a': ... case 'b': ... |
| 3. Default | default: return assertNever(x); |
| 4. Add new case | Compile error in every switch |
Discriminated Union
| Part | Example |
|---|---|
| Discriminant | status: 'success' |
| Branches | { status: 'success'; data: T } |
| Narrowing | switch (r.status) { case 'success': ... } |
| Exhaustive | default: assertNever(r) |
Best Practices
โ Do This:
// Use literal unions for closed sets
type Status = 'idle' | 'loading' | 'ready'; // โ
// Use `as const` for constants
const CONFIG = { mode: 'dark' } as const; // โ
// Extract unions from constants
type Mode = typeof CONFIG.mode; // โ
// Use `typeof X[number]` for array element unions
const COLORS = ['red', 'green'] as const;
type Color = typeof COLORS[number]; // โ
// Use `keyof typeof X` for key unions
type Key = keyof typeof CONFIG; // โ
// Use exhaustive switch with `assertNever`
default: return assertNever(x); // โ
// Use discriminated unions for variants
type Result = { status: 'ok'; value: T } | { status: 'err'; error: string }; // โ
// Use literal unions in function params
function log(msg: string, level: 'info' | 'warn' | 'error'): void { } // โ
// Import constants for runtime + types
import { STATUS } from './constants'; // โ
โ Don’t Do This:
// Don't use plain strings where a literal union fits
function setMode(m: string): void { } // โ ๏ธ accepts typos
// Don't forget `as const` on constant objects
const CONFIG = { mode: 'dark' }; // mode: string // โ ๏ธ widened
// Don't duplicate the union and the object
type Status = 'idle' | 'ready';
const Status = { Idle: 'idle', Ready: 'ready' }; // โ ๏ธ can drift
// Don't skip exhaustive checks
function message(s: Status): string {
if (s === 'idle') return '';
return ''; // silently handles 'ready' as '' // โ ๏ธ missed case
// Don't use `as const` on mutable data
const mutable = { count: 0 } as const;
mutable.count = 1; // โ
// Don't cast string variables to const
let x = 'hello';
const y = x as const; // still string // โ ๏ธ no effect
// Don't overuse `as const`
const data = getData() as const; // โ ๏ธ probably wrong
Common Pitfalls
| Pitfall | Problem | Solution |
|---|---|---|
Forgetting as const on constants | Wide types | Add as const |
Using let for literal constants | Widens | Use const |
| Duplicating union and object | Drift | Derive from one source |
| Skipping exhaustive check | Silent missing case | assertNever default |
| Missing narrowing on discriminant | No branch narrowing | Use a literal discriminant |
as const on variables | Doesn’t narrow existing string | Preserve at declaration |
Mutating as const object | Runtime error | Don’t mutate |
| Case-sensitivity | 'IDLE' โ 'idle' | Match exact strings |
| Numeric literals from JS | Widened to number | Annotate or as const |
| Array literal widening | string[] not literal tuple | as const |
Real-World Examples
1. Literal union type
type Status = 'idle' | 'loading' | 'ready' | 'error';
2. Const string
const mode = 'dark'; // 'dark'
3. Let widens
let mode = 'dark'; // string
4. as const object
const CONFIG = { mode: 'dark', retries: 3 } as const;
5. as const array
const COLORS = ['red', 'green'] as const;
6. Extract element union
type Color = typeof COLORS[number];
7. Extract key union
type Key = keyof typeof CONFIG;
8. Extract value union
type Value = typeof CONFIG[keyof typeof CONFIG];
9. Function param literal union
function log(level: 'info' | 'warn' | 'error'): void { }
10. Exhaustive switch
default: return assertNever(x);
11. assertNever helper
function assertNever(x: never): never {
throw new Error(`Unhandled: ${x}`);
}
12. Discriminated union
type Result =
| { status: 'ok'; value: string }
| { status: 'err'; error: string };
13. Numeric literal union
type Dice = 1 | 2 | 3 | 4 | 5 | 6;
14. Boolean literal type
type Yes = true;
15. Constant table with values
const STATUS = {
Idle: 'idle',
Ready: 'ready'
} as const;
16. Runtime constant use
setStatus(STATUS.Idle);
17. Literal union from runtime object
type Status = typeof STATUS[keyof typeof STATUS];
18. Function returning literal
function ready(): 'ready' { return 'ready'; }
19. Overloaded literal return
function parse(x: 'json'): object;
function parse(x: 'text'): string;
20. State machine transitions
function transition(state: State, event: Event): State { }
Visual: Literal Types Are Subtypes
โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ
โ string โ
โ โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ โ
โ โ 'loading' 'ready' 'error' โ โ
โ โ โ โ
โ โ literal types are narrower versions โ โ
โ โ โ โ
โ โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ โ
โ โ
โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ
'loading' assignable to string โ
string assignable to 'loading' โ
Visual: Widening Rules
โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ
โ let x = 'hello'; โ
โ โ โ
โ โผ โ
โ string (widened) โ
โ โ
โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ
โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ
โ const x = 'hello'; โ
โ โ โ
โ โผ โ
โ 'hello' (literal preserved) โ
โ โ
โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ
โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ
โ const obj = { a: 'x' }; โ
โ โ โ
โ โผ โ
โ { a: string } (property widens) โ
โ โ
โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ
โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ
โ const obj = { a: 'x' } as const; โ
โ โ โ
โ โผ โ
โ { readonly a: 'x' } (literal + readonly) โ
โ โ
โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ
Visual: as const Transformation
โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ
โ Before `as const` โ
โ โ
โ const CONFIG = { โ
โ apiUrl: 'https://x', โ
โ timeout: 5000, โ
โ features: ['auth'] โ
โ }; โ
โ โ
โ { apiUrl: string; timeout: number; โ
โ features: string[] } โ
โ โ
โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ
โ
โ as const
โผ
โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ
โ After `as const` โ
โ โ
โ { โ
โ readonly apiUrl: 'https://x'; โ
โ readonly timeout: 5000; โ
โ readonly features: readonly ['auth']; โ
โ } โ
โ โ
โ Every literal preserved, recursively โ
โ readonly โ
โ โ
โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ
Visual: Exhaustive Check
โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ
โ type Status = 'a' | 'b' | 'c'; โ
โ โ
โ function f(s: Status) { โ
โ switch (s) { โ
โ case 'a': return 1; โ
โ case 'b': return 2; โ
โ case 'c': return 3; โ
โ default: โ
โ return assertNever(s); โ
โ // s is never โ exhaustive โ
โ } โ
โ } โ
โ โ
โ Add 'd' to Status: โ
โ โ default branch โ s isn't never โ
โ โ assertNever(s) fails to compile โ
โ โ
โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ
Visual: Union Extraction Patterns
โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ
โ const COLORS = ['red', 'green'] as const; โ
โ โ
โ typeof COLORS โ readonly [...] โ
โ typeof COLORS[number] โ 'red' | 'green' โ
โ โ
โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ
โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ
โ const STATUS = { โ
โ Idle: 'idle', โ
โ Ready: 'ready' โ
โ } as const; โ
โ โ
โ keyof typeof STATUS โ 'Idle' | 'Ready' โ
โ typeof STATUS[keyof typeof STATUS]โ 'idle' | 'ready' โ
โ โ
โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ
Visual: Discriminated Union
โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ
โ type Result = โ
โ | { status: 'success'; data: string } โ
โ | { status: 'failure'; error: string } โ
โ | { status: 'loading' }; โ
โ โ
โ function display(r: Result) { โ
โ switch (r.status) { โ
โ case 'success': return r.data; โ
โ // r is Success โ
โ โ
โ case 'failure': return r.error; โ
โ // r is Failure โ
โ โ
โ case 'loading': return 'Loading...'; โ
โ // r is Loading โ
โ } โ
โ } โ
โ โ
โ Narrowing on the literal discriminant โ
โ โ
โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ
Visual: Literal Union vs Enum
โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ
โ Literal union โ
โ โ
โ type Status = 'idle' | 'ready'; โ
โ โ
โ โข Zero runtime code โ
โ โข Erased at compile โ
โ โข Integrates with narrowing โ
โ โข Preferred in modern TS โ
โ โ
โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ
โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ
โ Enum โ
โ โ
โ enum Status { Idle = 'idle', Ready = 'ready' }โ
โ โ
โ โข Emits runtime object โ
โ โข Provides values โ
โ โข Has reverse mapping (numeric) โ
โ โข Older pattern โ
โ โ
โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ
โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ
โ Best of both: โ
โ โ
โ const Status = { โ
โ Idle: 'idle', โ
โ Ready: 'ready' โ
โ } as const; โ
โ โ
โ type Status = โ
โ typeof Status[keyof typeof Status]; โ
โ โ
โ โข Named runtime constants โ
โ โข Zero runtime enum code โ
โ โข Type-safe at every use site โ
โ โ
โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ
Summary
| Concept | Meaning |
|---|---|
| Literal type | Single-value type โ 'a', 42, true |
| Literal union | Closed set โ 'a' | 'b' | 'c' |
const | Preserves literal types |
let | Widens to general types |
as const | Preserves literals, makes readonly |
typeof X[number] | Extract array element union |
keyof typeof X | Extract object key union |
typeof X[keyof typeof X] | Extract object value union |
| Discriminant | Literal field that picks a union branch |
| Discriminated union | Union of objects with a literal discriminator |
never | Empty type โ used for exhaustiveness |
assertNever | Helper that throws on unhandled values |
Key takeaways:
- Literal types describe exactly one value โ
'loading',42,true - They’re subtypes of their general types โ
'loading'is astring,42is anumber constpreserves literals;letwidens to the general type- Object and array properties widen even under
constโ useas constto preserve as constmakes the structure readonly and keeps every literal โ recursively- Literal unions (
'a' | 'b' | 'c') model closed sets โ the modern alternative to enums as constobjects are the single source of truth for constants and their types- Extract unions with
typeof X[number],keyof typeof X, andtypeof X[keyof typeof X] - Discriminated unions rely on literal discriminants for narrowing
- Exhaustive checks use
assertNeverto catch missing cases at compile time - Adding a new value to a literal union triggers compile errors in every place that needs updating
- Literal unions have zero runtime cost; enums emit a JavaScript object
Remember: Literal types are how TypeScript models closed sets โ the exact values a thing can be. They’re subtypes of string, number, and boolean, and they preserve their exact value only under const or as const. Combine them with unions for state machines and config, with as const objects for runtime constants, and with discriminated unions for variant types. The compiler then checks every value against every case โ turning accidental typos into compile errors and missing cases into failed builds.
Stop using slow, ad-bloated tool sites! ๐คฎ
๐ Search “KandZ Tools” on Google to use many professional utilities for free.
KandZ.me is the ultimate minimalist hub for:
โ
Finance (Mortgage, Interest, Inflation)
โ
Tech (Base64, JSON, Dev Suite, IP)
โ
Health (BMI, BMR, TDEE)
โ
Productivity (Timer, Workspace, QR)
โก๏ธ Fast & Private
๐ No data leaves your device
๐ 100% Free
๐ Use it now: https://tools.kandz.me
๐ Bookmark itโyouโll need it later!