Skip to Content
Course content

179: Literal Inference and const Contexts

Click on the "Edit" button in the top corner of the screen to edit your slide content.

I've spent a lot of time debugging "type widening" issues over the years. It's one of those TypeScript quirks that feels like the compiler is gaslighting you: you clearly wrote a specific string, but TypeScript insists it's just a generic string. Today, we're going to look at why that happens and how to use literal inference and const contexts to keep your types tight.

Designing the Notification Schema

Let's imagine we're building a notification system for a dashboard. We only want three specific types of alerts: 'success', 'error', and 'info'. I'll start by defining a type for this.

type NotificationType = 'success' | 'error' | 'info';

interface NotificationConfig {
  type: NotificationType;
  message: string;
}

Simple enough. Now, I want to create a default configuration object that I can reuse across the app.

The "Widening" Trip-up

Here is where I usually make my first mistake. I'll declare a variable to hold my default settings. I'm thinking, "I'm assigning 'success' to the type, so TypeScript obviously knows it's a NotificationType."

const defaultSettings = {
  type: 'success',
  message: 'Operation completed!',
};

// Later, I try to pass it to a function that expects NotificationConfig
function sendNotification(config: NotificationConfig) {
  console.log(`Sending ${config.type}: ${config.message}`);
}

sendNotification(defaultSettings); 
// ❌ Error: Type 'string' is not assignable to type 'NotificationType'.

Wait, what? I can see the code right there—it says 'success'. But here's the deal: because defaultSettings wasn't explicitly typed, TypeScript widened the type of type from the literal 'success' to the general string. Why? Because TypeScript assumes you might want to change defaultSettings.type to some other string later on.

Locking it down with as const

I could just add : NotificationConfig to the variable declaration, but sometimes you have deeply nested objects where that becomes tedious. This is where as const comes in. It tells TypeScript: "Don't widen anything in this object. Treat every value as a literal."

const defaultSettings = {
  type: 'success',
  message: 'Operation completed!',
} as const;

sendNotification(defaultSettings); // ✅ This works now!

By adding as const, I've turned the object into a read-only tuple/object where the type of type is exactly 'success', not string. It's a huge time-saver when you're dealing with configuration constants.

Understanding Implicit Const Contexts

Now, here's the interesting part. You don't always need as const. TypeScript is smart enough to recognize "const contexts"—places where it knows the value shouldn't widen because the target type is already specific.

Look at what happens when I define the variable with the type first:

const betterSettings: NotificationConfig = {
  type: 'success', // No error here!
  message: 'Operation completed!',
};

In this case, because the type property is being assigned to a slot that must be a NotificationType, TypeScript doesn't widen it to string. It infers the literal because it's in a "const context." The compiler basically says, "I know this needs to be one of those three specific strings, so I'll treat this assignment as a literal."

The rule of thumb I use: If you're creating a standalone config object that isn't immediately typed, use as const. If you're assigning a value to a property of a typed interface, let the const context do the work for you.




📋 Practical Task

Fixing the Theme Configuration Widening

You are building a theme engine. You have a type ThemeMode which can only be 'light', 'dark', or 'high-contrast'. However, the current implementation is failing because the defaultTheme object is being widened to string, causing a type error when passed to the applyTheme function.

Your Task: Use as const to ensure the defaultTheme object maintains its literal types so that it can be passed into applyTheme without errors.

type ThemeMode = 'light' | 'dark' | 'high-contrast';

interface ThemeConfig {
  mode: ThemeMode;
  primaryColor: string;
}

function applyTheme(config: ThemeConfig) {
  console.log(`Applying ${config.mode} theme...`);
}

// FIX THIS OBJECT:
const defaultTheme = {
  mode: 'light',
  primaryColor: '#ffffff',
};

applyTheme(defaultTheme);
Rating
0 0

There are no comments for now.

to be the first to leave a comment.