TypeScript
Completed
-
Section 1: Getting Started
-
Section 2: Basic Types
-
Section 3: Functions and Objects
-
Section 4: Advanced Types
-
Section 5: Object-Oriented TypeScript
-
Section 6: Working with Modules
-
Section 7: Tooling and Practice
-
Section 8: Type-Level Programming
-
Section 9: TypeScript with Backends
-
Section 10: Testing Typed Code
-
Section 11: Data Validation with Types
-
Section 12: Practical Projects
-
Section 13: Compiler Internals
-
Section 14: Configuration Deep Dive
-
Section 15: Enums, Symbols, and Special Types
-
Section 16: Working with Async Code
-
Section 17: TypeScript and the DOM
-
Section 18: Advanced Generics Practice
-
Section 19: Working with Third-Party Types
-
Section 20: Monorepo and Large-Scale Practices
-
Section 21: Common Pitfalls and Best Practices
-
Section 22: Interview Practice
-
Section 23: Handbook: Narrowing In Depth
-
Section 24: Handbook: Object Types In Depth
-
Section 25: Handbook: Classes In Depth
-
Section 26: Handbook: Modules In Depth
-
Section 27: Handbook: Declaration Files In Depth
-
Section 28: JSX and Namespaces
-
Section 29: Compiler Configuration Reference
-
Section 30: More Practice Exercises
-
Section 31: Handbook: Everyday Types Deep Dive
-
Section 32: Utility Types Full Reference
-
Section 33: Decorators Reference
-
Section 34: Mixins and Advanced OOP Patterns
-
Section 35: Iterators and Generators Typing
-
Section 36: More Type-Level Programming Practice
-
Section 37: TypeScript Ecosystem Tools
-
Section 38: TypeScript with Testing Frameworks
-
Section 39: TypeScript for Library Authors
-
Section 40: More Interview Practice
-
Section 41: Handbook: Functions In Depth
-
Section 42: Handbook: Type Manipulation Deep Dive
-
Section 43: More Real-World Patterns
-
Section 44: TypeScript Release Notes Highlights
-
Section 45: Final Practice and Review
179: Literal Inference and const Contexts
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);
There are no comments for now.