Literal Type in TypeScript
TypeScript’s expressive type system relies heavily on Literal Types, which allow you to define types according to the precise values of JavaScript primitives. They let you reduce the range of values that a type can store, from a general category (like string
or number
) to a single, targeted value (like 42
or 'Mon'
).
Primitive types, such as string
, number
, and boolean
, can be thought of as massive unions of all possible matching literal values. You are choosing just one particular member from that set when you use a literal type.
Types of Primitive Literals
Booleans, strings, and integers can all have literal types supported by TypeScript.
String Literal Types
Only a single string value can be stored in a variable or property thanks to string literal types. They frequently function as a string-based enum and are quite helpful when paired with union types to depict tightly confined collections of possibilities.
Define a direction type, for instance:
type CardinalDirection = "North" | "East" | "South" | "West";
function move(distance: number, direction: CardinalDirection) {
// ... logic constrained to valid directions
}
move(1, "North"); // Okay
move(1, "Nurth"); // Error: Type '"Nurth"' is not assignable to type 'CardinalDirection'.
If a variable is explicitly typed as a single string literal, any other string value will result in a compilation error:
let greeting: 'Hello';
greeting = 'Hello'; // Okay
greeting = 'Hi'; // Error: Type '"Hi"' is not assignable to type '"Hello"'.
Number and Boolean Literal Types
By fixing the type to a precise numerical value or particular boolean state, integer and boolean literal types which work just like string literals are also supported by TypeScript.
type OneToFive = 1 | 2 | 3 | 4 | 5;
type Bools = true | false;
let age: 42 = 42; // age must always be 42
// age = 43; // Error: Type '43' is not assignable to type '42'.
Distinguishing Literals and Primitives
Awareness when a literal type is automatically applied requires an awareness of the difference between how TypeScript infers types for declarations made using let
(or var
) and const
. The idea of Type Widening is what makes this difference possible.
Type Widening and Mutability
When a variable is deemed changeable, TypeScript’s action known as “Type Widening” causes it to generalise a particular value to its more general primitive type.
let
(Mutable Variables) -> Primitive Types (Wider) TypeScript thinks that the value of a variable declared withlet
(orvar
) is preliminary and that you may reassign it to another value of the same base type at a later time. TypeScript “widens” the type from the specific literal to the general primitive type (string
,number
, orboolean
) because the value is subject to change.- Because of this flexibility, any value that is compatible with the inferred primitive type can be stored in the variable:
const
(Immutable Variables) -> Literal Types (Narrower) TypeScript is aware that the variable reference cannot be changed once it has been created when a primitive value is declared usingconst
. TypeScript reliably infers the most detailed type possible the exact literal value itself because the value of the variable is assured to never change.- As a result, the variable is limited to that one provided value, creating a narrower type:
Code Example Demonstrating Inference
Let’s show how type safety is affected when interacting with functions that demand literal types because const
maintains the literal type while let
widens the type.
For example, the function iTakeOnlyMon
requires the literal string 'Mon'
as a requirement:
function iTakeOnlyMon(day: 'Mon'): void {
console.log(`Processing report for: ${day}`);
}
// 1. Declaration using let (WIDENING)
let dayOfWeekLet = 'Mon';
// Inferred type: string
console.log(`Type of dayOfWeekLet: ${typeof dayOfWeekLet}`);
// Output: Type of dayOfWeekLet: string
// 2. Declaration using const (LITERAL INFERENCE)
const dayOfWeekConst = 'Mon';
// Inferred type: 'Mon'
console.log(`Type of dayOfWeekConst: ${typeof dayOfWeekConst}`);
// Output: Type of dayOfWeekConst: Mon
// --- Usage ---
iTakeOnlyMon(dayOfWeekConst); // OK: 'Mon' (literal) is assignable to 'Mon' (literal)
iTakeOnlyMon(dayOfWeekLet);
// Error: Argument of type 'string' is not assignable to parameter of type 'Mon'.
// TypeScript complains because dayOfWeekLet is of type string, which is wider
// than the required literal type 'Mon', even though its current value is 'Mon'.
Explanation of Outputs and Errors:
When dayOfWeekLet
is declared using let
, TypeScript infers the general string
type. Although the variable currently holds the value 'Mon'
, TypeScript cannot guarantee it won’t change to 'Tue'
later. Because the string
type set is infinitely wide (allowing any string), it is not assignable to the narrower literal type 'Mon'
.
On the other hand, TypeScript infers the precise literal type 'Mon'
when dayOfWeekConst
is declared using const
. The assignment is allowed and type safe because this literal type is the same as the argument type of the function.
Const Assertions for Non-Primitives
Note that only primitive values are consistently covered by this automatic literal inference for const
. TypeScript usually fixes the variable reference (preventing reassignment of the object itself) when you define an object literal using const
. However, it usually expands the types of the object’s properties (for example, a number property will still be inferred as number
, not the exact literal value).
You must use the as const
assertion to enforce recursive literal typing, in which all properties are marked as readonly
and handled as their narrowest literal type.
Use Cases for Literal Types
Literal types are very helpful in enhancing readability and code safety, particularly in complex situations:
Union Types and Literals
As previously demonstrated, a composite type that explicitly defines a tiny, closed set of admissible values is produced when many literal types are combined into a Union Type (|). This is very typical when describing state machines or configuration options.
Discriminated Unions
Within Union Types of objects (or interfaces), literal types act as discriminants. TypeScript can “narrow down” the type of the entire object by examining the value of a property with a literal type (the discriminant), granting safe access to only those properties that are known to exist on that particular variant.
For example, type checking can be safely carried out inside conditional blocks by using a literal type 'foo'
or 'bar'
on a kind
property:
type Foo = { kind: 'foo', foo: number }
type Bar = { kind: 'bar', bar: number }
type FooOrBar = Foo | Bar;
function doStuff(arg: FooOrBar) {
if (arg.kind === 'foo') {
console.log(arg.foo); // OK, arg is narrowed to type Foo
// console.log(arg.bar); // Error! arg is not guaranteed to have 'bar'
} else { // MUST BE Bar! [38]
console.log(arg.bar); // OK, arg is narrowed to type Bar
}
}
TypeScript can refine types based on code flow analysis by employing literal types within type guards (such as typeof
or testing equality against literals). This ensures that actions are only carried out if the type is guaranteed to be right within that scope.
In Conclusion
Literal types give the highest level of type safety by enabling the definition of precisely limited types based on exact values. The main distinction between let
and const
in this context is that let
necessitates explicit annotation or produces the wider primitive type because of the potential for future mutation, whereas const
allows automated inference of these narrow literal types for primitives.