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
97: The satisfies Operator
I've been running into a recurring annoyance in my projects lately: the tension between wanting to ensure an object follows a specific schema and wanting TypeScript to remember exactly what's inside that object. Let's look at a real example from a UI project I'm working on.
The Widening Trap
Imagine we're building a theme system. We want our theme objects to have a set of colors, but we want to allow those colors to be either a predefined palette key or any arbitrary CSS color string. Here is how I'd typically set that up:
type Palette = "primary" | "secondary" | "accent";
type Theme = {
readonly [key: string]: Palette | string;
};
const theme: Theme = {
brandColor: "primary",
surfaceColor: "#ffffff",
borderColor: "secondary",
};
At first glance, this is perfect. The : Theme annotation ensures I don't accidentally put a number or a boolean in there. But here is where it gets frustrating. Let's say I have a function that specifically needs a Palette key to look up a value in a global CSS variable map:
function getPaletteValue(color: Palette) {
const map = { primary: "#007bff", secondary: "#6c757d", accent: "#ffc107" };
return map[color];
}
// TypeScript yells at me here:
// Argument of type 'string | Palette' is not assignable to parameter of type 'Palette'.
getPaletteValue(theme.brandColor);
Wait, what? I can clearly see that brandColor is `"primary"`. But because I annotated theme as Theme, TypeScript "widened" the type. It now thinks theme.brandColor could be any string or any Palette key. It lost the specific knowledge that it was exactly `"primary"`.
The Dangerous Workaround
Now, I could use a type assertion. I've seen a lot of developers do this to "silence" the compiler:
getPaletteValue(theme.brandColor as Palette);
Sure, the red squiggle is gone. But I hate this. I've just told TypeScript, "Trust me, I know what I'm doing," which effectively turns off the safety check. If I accidentally changed brandColor to "#ff0000" in the object definition, the assertion would still pass, and I'd have a runtime bug.
Finding the Middle Ground
This is exactly why the satisfies operator was introduced. It allows us to validate that an object matches a type without actually changing the resulting type of that object. Let's try it out by replacing the colon with the satisfies keyword.
const theme = {
brandColor: "primary",
surfaceColor: "#ffffff",
borderColor: "secondary",
} satisfies Theme;
This feels subtle, but it changes everything. I'm telling TypeScript: "Make sure this object matches the Theme interface, but leave the inferred type of the object alone."
Now, let's try that function call again:
// This now works perfectly!
getPaletteValue(theme.brandColor);
TypeScript looks at the object, confirms it satisfies the Theme type, and then preserves the narrowest possible type for each property. It knows theme.brandColor is specifically the literal "primary", which is a valid Palette.
When to use which?
I've started following a simple rule of thumb: use : Type when you want to strictly define what a variable is (especially for function parameters or class properties). Use satisfies Type when you want to verify that a value is correct, but you still want to keep the specific details of that value for later use.
📋 Practical Task
Refactoring the Game Character Stat Validator
You are working on a RPG engine. You have a CharacterStats type that ensures every character has a set of numeric attributes. However, you have a specific function calculateCriticalHit that only works if the strength attribute is specifically a positive number (not just any number).
Currently, the code uses a type annotation, which is causing the compiler to lose the specific literal value of the stats, leading to a type error in the critical hit calculation.
Your Task: Refactor the hero object definition to use the satisfies operator instead of a type annotation. This should allow the calculateCriticalHit function to accept hero.strength without needing a type assertion (as).
type CharacterStats = {
strength: number;
agility: number;
intelligence: number;
};
function calculateCriticalHit(power: 10 | 20 | 30) {
return power * 2;
}
// FIX THIS LINE:
const hero: CharacterStats = {
strength: 20,
agility: 15,
intelligence: 10,
};
// This line currently throws a type error because 'strength' is widened to 'number'
console.log(calculateCriticalHit(hero.strength));There are no comments for now.