Reading a property by a computed name is ordinary JavaScript: config[name], translations[locale], handlers[event.type]. In TypeScript it’s also where a lot of people first meet error TS7053 and reach for as any. The two operators that fix it properly, keyof and indexed access types, are small, and they show up in almost every generic utility you’ll be asked to write in an interview.
The TS7053 error and what it’s protecting
Here’s a feature-flag lookup:
const FLAGS = { darkMode: false, betaSearch: true, newCheckout: false };
function isEnabled(name: string): boolean {
return FLAGS[name];
}key1.ts(4,10): error TS7053: Element implicitly has an 'any' type because expression of type 'string' can't be used to index type '{ darkMode: boolean; betaSearch: boolean; newCheckout: boolean; }'.
No index signature with a parameter of type 'string' was found on type '{ darkMode: boolean; betaSearch: boolean; newCheckout: boolean; }'.FLAGS has exactly three known keys. name could be any string, including "darkmode" with a lowercase m, or "toString". TypeScript doesn’t know what FLAGS["anything"] is, so the result would be an implicit any, and noImplicitAny (part of strict) rejects that. The error isn’t about the lookup being impossible; it’s about the result type being unknown.
keyof typeof: a union of the real keys
typeof FLAGS in a type position gives you the object’s type. keyof turns that type into a union of its keys:
type FlagName = keyof typeof FLAGS; // "darkMode" | "betaSearch" | "newCheckout"
function isEnabledOk(name: FlagName): boolean {
return FLAGS[name];
}
isEnabledOk("darkmode");key1.ts(11,13): error TS2345: Argument of type '"darkmode"' is not assignable to parameter of type '"betaSearch" | "darkMode" | "newCheckout"'.Now the lookup type-checks, because every possible name is a real key with a boolean value. The typo moves from a runtime undefined to a compile error at the call site, and editors autocomplete the three names. The key list also stays in sync automatically: add a flag to the object and the type grows with it. (Recent compilers print union members in a stable sorted order, which is why the message lists "betaSearch" first.)
One detail about keyof on index signatures catches people out in multiple-choice questions:
type Dict = { [key: string]: number };
type DK = keyof Dict; // string | numberIt’s string | number, not string, because JavaScript converts numeric property keys to strings, so dict[0] and dict["0"] are the same lookup and TypeScript allows both.
Indexed access types: T[K]
Square brackets work in type positions too. T[K] means “the type of property K on T”:
type Order = { id: string; total: number; lines: { sku: string; qty: number }[] };
type Total = Order["total"]; // number
type Line = Order["lines"][number]; // { sku: string; qty: number }
type IdOrTotal = Order["id" | "total"]; // string | number
type Bad = Order["customer"];key1.ts(25,18): error TS2339: Property 'customer' does not exist on type 'Order'.Three patterns are worth knowing. A single key gives that property’s type. A union of keys gives the union of their types. And indexing an array type with number gives its element type, which is the shortest way to name “one line of an order” without declaring a separate interface. A key that doesn’t exist is an error, so these derived types break loudly when the source changes, which is what you want.
Note that the index must be a type. With const key = "total", writing Order[key] fails because key is a value, and the compiler says so directly: 'key' refers to a value, but is being used as a type here. Did you mean 'typeof key'? Writing Order[typeof key] works, because typeof key is the literal type "total". The same confusion between the value world and the type world explains why keyof FLAGS fails and keyof typeof FLAGS works.
Combined with generics, this pair gives you the signature behind every typed getter:
function get<T, K extends keyof T>(obj: T, key: K): T[K] {
return obj[key];
}The return type follows the key: get(order, "total") is number, and get(order, "lines") is the line array.
Why Object.keys returns string[]
Sooner or later you’ll loop over an object and hit TS7053 again:
for (const key of Object.keys(FLAGS)) {
console.log(FLAGS[key]); // TS7053: 'key' is string
}Plenty of people consider this a TypeScript bug. It’s deliberate, and explaining why is a favourite interview follow-up. TypeScript’s types describe the minimum shape of an object, not its exact contents. A value typed { darkMode: boolean } can legally be an object with more properties:
type Flags = { darkMode: boolean };
function listFlags(flags: Flags) {
return Object.keys(flags);
}
const fromServer = { darkMode: true, legacyHeader: "yes" };
console.log(listFlags(fromServer)); // [ 'darkMode', 'legacyHeader' ]That compiles and runs. If Object.keys returned (keyof Flags)[], the type would claim the array contains only "darkMode", and code indexing a lookup table with those keys would fail at runtime on "legacyHeader". So string[] is the honest type.
When you control the object and know it has no extras, like a const literal in the same module, a local cast is reasonable: (Object.keys(FLAGS) as FlagName[]). Keep it next to the literal so the assumption is visible. Object.entries has the same reasoning and the same workaround.
Accepting keys from outside
Flags often arrive as strings from a query parameter or an admin panel. You need to turn an arbitrary string into a FlagName, and the check must happen at runtime:
function isFlagName(name: string): name is FlagName {
return Object.hasOwn(FLAGS, name);
}
function isEnabled(name: string): boolean {
return isFlagName(name) ? FLAGS[name] : false;
}
console.log(isEnabled("betaSearch"), isEnabled("toString"), "toString" in FLAGS);
// true false trueThat output is from running the compiled code. Object.hasOwn checks only the object’s own properties. The in operator walks the prototype chain, so "toString" in FLAGS is true, and a guard written with in would accept "toString" and return a function where the type promised a boolean. When the keys come from users, that difference matters.
One more setting is worth knowing. With a Record<string, number>, prices["banana"] is typed number even though the key may not exist. Turning on noUncheckedIndexedAccess makes such lookups number | undefined; on the same code it reports Type 'number | undefined' is not assignable to type 'number'. It isn’t part of strict, but it’s a good choice for codebases that index records with external keys.
Interview practice
You have const messages = { en: { greeting: "Hello" }, fr: { greeting: "Bonjour" } }. Write a function t(locale, key) where locale must be a known locale, key must be a key of that locale’s messages, and the return type is string. Then say how you’d handle a locale string from the browser.
Answer
type Locale = keyof typeof messages;
type MessageKey = keyof (typeof messages)["en"];
function t(locale: Locale, key: MessageKey): string {
return messages[locale][key];
}Locale is "en" | "fr", from keyof typeof. MessageKey uses an indexed access type to reach the English bundle and then takes its keys, treating English as the source of truth. If French is missing a key, messages[locale][key] stops compiling, because fr would no longer have every MessageKey, which catches incomplete translations at build time. A stricter version makes that explicit by typing messages with satisfies Record<string, Record<MessageKey, string>>.
For navigator.language, which is an arbitrary string like "fr-CA", I’d normalise it to the base language and pass it through a guard such as isLocale(s: string): s is Locale that uses Object.hasOwn(messages, s), falling back to "en". I wouldn’t cast the browser string to Locale, because then an unsupported language would reach messages[locale] and read undefined.