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

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:

  1. Create an interface called PlayerStats.
  2. Add a mandatory property playerName (string) and level (number).
  3. Implement an index signature that allows any other string key to hold a number value.
  4. Create a player1 object using this interface. Give it a name, a level, and at least three custom stats (e.g., strength, intelligence, charisma).
  5. Try to assign a string value to one of the custom stats and observe the TypeScript error.
Rating
0 0

There are no comments for now.

to be the first to leave a comment.