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
22: Index Signatures
I was working on a project recently where I had to implement a translation system. The goal was simple: a store that holds a bunch of keys (like "welcome_message" or "error_not_found") and maps them to their translated strings. My first instinct was to be precise, because that's what we're taught to do with TypeScript.
The rigid interface problem
I started by defining an interface for my translation bundle. I knew a few of the keys I'd definitely need, so I wrote it like this:
interface TranslationBundle {
welcome: string;
logout: string;
}
const enUS: TranslationBundle = {
welcome: "Welcome back!",
logout: "Sign out",
};
This works great until the project grows. I suddenly realized that different pages have different keys, and I can't possibly hard-code every single translation string in the interface. I tried to add a new key on the fly:
enUS.settings_title = "User Settings";
// ❌ Error: Property 'settings_title' does not exist on type 'TranslationBundle'.
TypeScript is doing its job here—it's preventing me from adding random properties to an object. But in this specific case, the "randomness" is the whole point. I don't know the keys in advance; they're coming from a JSON file provided by the localization team.
Opening it up
I could just use any or a Record<string, string>, but I still want the TranslationBundle to feel like a proper type that I can pass around my app. I need a way to tell TypeScript: "I know about welcome and logout, but honestly, any other string key is fine as long as the value is also a string."
This is where the index signature comes in. I'll add a special property definition that acts as a catch-all:
interface TranslationBundle {
welcome: string;
logout: string;
[key: string]: string; // This is the index signature
}
const enUS: TranslationBundle = {
welcome: "Welcome back!",
logout: "Sign out",
settings_title: "User Settings", // Now this is perfectly legal
privacy_policy: "Read our policy", // This is too
};
By adding [key: string]: string, I'm telling the compiler that this object can be indexed by any string, and the resulting value will always be a string. Now I have the best of both worlds: autocomplete for the keys I know exist, and flexibility for the ones I don't.
The catch with mixed types
Now, here is where things get tricky. I decided I wanted to add a version number to my bundle so I could track which version of the translations I was using. I tried this:
interface TranslationBundle {
welcome: string;
logout: string;
version: number; // I want this to be a number
[key: string]: string;
}
// ❌ Error: Property 'version' of type 'number' is not assignable to string index signature.
TypeScript just blocked me. Why? Because the index signature [key: string]: string is a promise. It says, "If you give me any string key, I guarantee you'll get a string back." By adding version: number, I'm breaking that promise. If I tried to loop through the keys of the object, I'd eventually hit version and get a number when I was expecting a string.
To fix this, I have two options. I can either make the index signature more permissive by using a union type, or I can move the metadata to a separate object. If I want to keep it in one place, I'll do this:
interface TranslationBundle {
welcome: string;
logout: string;
version: number;
[key: string]: string | number; // Now both are allowed
}
It's a small change, but it's a crucial one. Just keep in mind that when you use a union in an index signature, you'll have to use type guards (like typeof) when accessing those values to make sure you're dealing with the type you expect.
📋 Practical Task
Build a Dynamic Game Stat Tracker
You are building a system to track player statistics in an RPG. Some stats are mandatory (like playerName and level), but the game allows for an unlimited number of custom "buffs" or "attributes" (like strength, agility, or luck) that are all numeric values.
Your Task:
- Create an interface called
PlayerStats. - Add a mandatory property
playerName(string) andlevel(number). - Implement an index signature that allows any other string key to hold a
numbervalue. - Create a
player1object using this interface. Give it a name, a level, and at least three custom stats (e.g.,strength,intelligence,charisma). - Try to assign a string value to one of the custom stats and observe the TypeScript error.
There are no comments for now.