Exploring TypeScript Literal Type Widening

Discover how to use literal type widening to your advantage in TypeScript.

Brandon Evans
Bits and Pieces

--

Photo by Anthony Da Cruz on Unsplash

As TypeScript continues to grow in popularity, having a deep understanding of its features becomes increasingly important for developers. In this article, We will discuss TypeScript literal types, type widening and narrowing, and best practices for using literal types.

String Literal Types

String literal types are a way to specify the exact string values that a variable can have. They are useful when you want to restrict a variable to a set of specific string values. Here’s an example:

type Direction = "north" | "east" | "south" | "west";
let dir: Direction = "north";

In this example, dir can only be assigned one of the four specified string values. Attempting to assign it any other string value will result in a type error.

Numeric Literal Types

Numeric literal types work similarly to string literal types but are used to specify exact numeric values. Here’s an example:

type DiceRoll = 1 | 2 | 3 | 4 | 5 | 6;
let roll: DiceRoll = 4;

In this example, roll can only be assigned one of the six specified numeric values.

Boolean Literal Types

Boolean literal types are the simplest form of literal types and can be either true or false. Although rarely used on their own, they can be combined with other literal types to create more complex type definitions.

Type Widening in TypeScript

Type widening is the process by which TypeScript infers a more general type for a variable when it cannot determine the exact type. This can happen when a variable is initialized with a literal value or when it is assigned a value later in the code. Type widening can lead to unintended type errors if not properly understood and managed.

Literal Type Widening

Literal type widening occurs when TypeScript infers a more general type from a literal value. For example:

let dir = "north";
dir = "east"; // No error, even though we intended to restrict the value of 'dir'

In this case, TypeScript infers the type of dir as string, not the more specific literal type "north". This means that any string value can be assigned to dir, even though we intended to restrict it to specific values. To avoid this, we can explicitly provide a type annotation:

let dir: "north" = "north";
dir = "east"; // Error: Type '"east"' is not assignable to type '"north"'.

Now, TypeScript enforces the specific literal type, and attempting to assign any other value results in a type error.

Type Narrowing

Type narrowing is the opposite of type widening: TypeScript infers a more specific type for a variable based on context, such as in a conditional statement or a type guard. This can help prevent errors and make your code more robust. Here’s an example:

function processDirection(direction: "north" | "south" | "east" | "west") {
if (direction === "north" || direction === "south") {
console.log("Moving vertically");
} else {
console.log("Moving horizontally");
}
}

In this example, TypeScript narrows the type of direction based on the conditional statement, allowing the code to execute correctly without type errors.

💡 Pro Tip: If you needed to reuse these types across all your projects, an open-source toolchain such as Bit can help you share types across multiple projects, considerably minimizing boilerplate.

Learn more here:

Best Practices for Using Literal Types

To make the most of literal types and avoid potential issues related to type widening and narrowing, follow these best practices:

Const Assertions

Const assertions can be used to prevent type widening by telling TypeScript to infer the most specific type possible for a value. To use a const assertion, add as const to a literal value. Here's an example:

const direction = "north" as const;
direction = "east"; // Error: Cannot assign to 'direction' because it is a constant.

Type Aliases

Type aliases are useful for creating reusable and more readable type definitions, especially when working with literal types. Here’s an example:

type Direction = "north" | "south" | "east" | "west";
let dir: Direction = "north";

Union Types

Union types allow you to create more complex type definitions by combining multiple types, including literal types. Here’s an example:

type Coordinate = [number, number, "x" | "y"];
let point: Coordinate = [3, 4, "x"];

Real-World Use Cases

Literal types can be used in various real-world scenarios, such as:

Modeling Enums

Enums are a common programming construct that can be modeled using literal types in TypeScript:

type LogLevel = "debug" | "info" | "warn" | "error";
function log(level: LogLevel, message: string) {
// Logging logic goes here
}

Discriminated Unions

Discriminated unions are a powerful TypeScript feature that leverages literal types to create more expressive and type-safe data structures:

interface Circle {
kind: 'circle';
radius: number;
}

interface Square {
kind: 'square';
sideLength: number;
}
type Shape = Circle | Square;

function area(shape: Shape): number {
switch (shape.kind) {
case 'circle':
return Math.PI * shape.radius ** 2;
case 'square':
return shape.sideLength ** 2;
default: // Ensures exhaustive checking of Shape types
const _exhaustiveCheck: never = shape;
return _exhaustiveCheck;
}
}

In this example, we define two different shape interfaces and a discriminated union type Shape. The kind property serves as a discriminator, allowing TypeScript to determine the correct type in the area function.

Configuration Objects

Literal types can be used to create type-safe configuration objects for libraries or applications:

interface AppConfig {
logLevel: "debug" | "info" | "warn" | "error";
theme: "light" | "dark";
}

function initApp(config: AppConfig) {
// Application initialization logic goes here
}

const config: AppConfig = {
logLevel: "info",
theme: "dark",
};

initApp(config);

Conclusion

Literal type widening is an essential concept to understand when working with TypeScript. It can help you leverage the full power of TypeScript’s type system and write more robust and type-safe code.

Thanks for reading! If you’re not yet a Medium member, consider becoming one to support me here, which gives you unlimited access to everything on Medium.

Build Apps with reusable components, just like Lego

Bit’s open-source tool help 250,000+ devs to build apps with components.

Turn any UI, feature, or page into a reusable component — and share it across your applications. It’s easier to collaborate and build faster.

Learn more

Split apps into components to make app development easier, and enjoy the best experience for the workflows you want:

Micro-Frontends

Design System

Code-Sharing and reuse

Monorepo

--

--