Page Content

Tutorials

What is a Literal Type in TypeScript?

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.

  1. let (Mutable Variables) -> Primitive Types (Wider) TypeScript thinks that the value of a variable declared with let (or var) 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, or boolean) because the value is subject to change.
  2. Because of this flexibility, any value that is compatible with the inferred primitive type can be stored in the variable:
  3. 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 using const. TypeScript reliably infers the most detailed type possible the exact literal value itself because the value of the variable is assured to never change.
  4. 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.

Index