TypeScript keyof string confusion has one cause: keyof T isn’t always a string. In a generic it’s string | number | symbol, so narrow it with keyof T & string, and turn a runtime string into a key with a type guard:
function isKeyOf<T extends object>(obj: T, key: PropertyKey): key is keyof T {
return Object.hasOwn(obj, key);
}
My test setup was TypeScript 7.0.2 and Node.js v22.22.2 with --strict on. Each output below, compiler errors included, is pasted from that terminal.
Why is keyof not just string in TypeScript?
Object keys can be strings, numbers or symbols, so for an unknown T, keyof T could be any of them. My TypeScript keyof operator guide covers the basics; here’s the string side:
interface Customer { id: number; name: string; city: string }
type Scores = { [team: string]: number };
const k: keyof Customer = "zip"; // only "id" | "name" | "city"
const s: keyof Scores = true; // reveals what keyof Scores is
function label<T>(key: keyof T): string {
return key.toUpperCase(); // key may be a number or a symbol
}
function heading<T>(key: keyof T): string {
return `Column: ${key}`; // symbols can't go into a template string
}
Output:
keyof_union.ts(4,7): error TS2322: Type '"zip"' is not assignable to type 'keyof Customer'.
keyof_union.ts(5,7): error TS2322: Type 'boolean' is not assignable to type 'string | number'.
keyof_union.ts(8,14): error TS2339: Property 'toUpperCase' does not exist on type 'string | number | symbol'.
Property 'toUpperCase' does not exist on type 'number'.
keyof_union.ts(12,21): error TS2731: Implicit conversion of a 'symbol' to a 'string' will fail at runtime. Consider wrapping this expression in 'String(...)'.

keyof Scores; TS2339 and TS2731 come from the generic key.Two details may surprise you. A string index signature gives string | number, since obj[1] and obj["1"] are the same property. And template strings reject the key, because converting a symbol throws.
Get only string keys with keyof T & string or Extract
Intersecting with string drops number and symbol keys. Extract<keyof T, string> does the same as a named helper:
function label<T>(key: keyof T & string): string {
return key.toUpperCase();
}
type StringKeys<T> = Extract<keyof T, string>;
const meta = Symbol("meta");
const store = { id: 7, city: "Austin", 2026: "fiscal year", [meta]: true };
type AllKeys = keyof typeof store; // "id" | "city" | 2026 | typeof meta
const textKeys: StringKeys<typeof store>[] = ["id", "city"];
console.log(textKeys.map((k) => label<typeof store>(k)));
console.log("Object.keys:", Object.keys(store));
Output:
[ 'ID', 'CITY' ]
Object.keys: [ '2026', 'id', 'city' ]
At runtime, Object.keys() skips the symbol and returns 2026 first, as a string. For loops over keys, see iterating over objects in TypeScript.
Convert a string to keyof T with a type guard
Strings from a URL, a form or JSON are just string, and you can’t index a typed object with them until you prove they’re real keys. A generic type guard with Object.hasOwn() checks at runtime and works for any object:
const prices = { basic: 9.99, pro: 19.99, team: 49.99 };
type Plan = keyof typeof prices; // "basic" | "pro" | "team"
function isKeyOf<T extends object>(obj: T, key: PropertyKey): key is keyof T {
return Object.hasOwn(obj, key);
}
for (const input of ["pro", "team", "enterprise", "toString"]) {
if (isKeyOf(prices, input)) { // input is now Plan
console.log(`${input.padEnd(10)} $${prices[input].toFixed(2)}`);
} else {
console.log(`${input.padEnd(10)} not a plan`);
}
}
console.log("'toString' in prices:", "toString" in prices);
console.log("Object.hasOwn: ", Object.hasOwn(prices, "toString"));
const forced = "enterprise" as Plan; // compiles, but it's a lie
try {
console.log(prices[forced].toFixed(2));
} catch (err) {
console.log("cast:", (err as Error).message);
}
Output:
pro $19.99
team $49.99
enterprise not a plan
toString not a plan
'toString' in prices: true
Object.hasOwn: false
cast: Cannot read properties of undefined (reading 'toFixed')

I use Object.hasOwn() instead of in on purpose: in also finds inherited members, so "toString" would pass. The key is keyof T return type is explained in the is keyword in TypeScript.
Notice keyof typeof prices: when you have an object but no interface, typeof gets its type and keyof lists the keys. Strings to enum members work the same way; see converting a string to an enum in TypeScript.
The as Plan cast compiles but checks nothing, so it crashed. I cover when casts are fine in TypeScript type casting.
Fix TS7053: expression of type string can’t be used to index
Indexing a typed object with a plain string, or with a for...in key, gives TS7053:
const prices = { basic: 9.99, pro: 19.99, team: 49.99 };
function priceFor(plan: string): number {
return prices[plan];
}
for (const key in prices) {
console.log(prices[key]);
}
Output:
keyof_ts7053.ts(4,10): error TS7053: Element implicitly has an 'any' type because expression of type 'string' can't be used to index type '{ basic: number; pro: number; team: number; }'.
No index signature with a parameter of type 'string' was found on type '{ basic: number; pro: number; team: number; }'.
keyof_ts7053.ts(8,15): error TS7053: Element implicitly has an 'any' type because expression of type 'string' can't be used to index type '{ basic: number; pro: number; team: number; }'.
No index signature with a parameter of type 'string' was found on type '{ basic: number; pro: number; team: number; }'.

for...in loop index with a string.You have three honest fixes. Use the type guard above (type narrowing at work), cast with Object.keys(prices) as Plan[] when the keys came from that same object, or type the object as Record<string, number> when any string is allowed.
With that last option, you’ll get undefined for unknown keys, so handle it. TypeScript Record explains when that type fits.
Build template literal keys from keyof
Template literal types turn string keys into new names. Here, address fields become event names and getters, with Capitalize fixing the casing:
interface Address { street: string; city: string; state: string; zip: string }
type AddressEvent = `${keyof Address & string}Changed`;
type Getters<T> = { [K in keyof T & string as `get${Capitalize<K>}`]: () => T[K] };
const home: Address = { street: "500 Congress Ave", city: "Austin", state: "TX", zip: "78701" };
const getters: Getters<Address> = {
getStreet: () => home.street,
getCity: () => home.city,
getState: () => home.state,
getZip: () => home.zip,
};
function on(event: AddressEvent, handler: () => void): void {
console.log("listening for", event);
handler();
}
on("cityChanged", () => console.log("city is now", getters.getCity()));
// @ts-expect-error - "zipCodeChanged" is not an AddressEvent
on("zipCodeChanged", () => {});
Output:
listening for cityChanged
city is now Austin
listening for zipCodeChanged

cityChanged is valid and getCity returns Austin.You need & string here, because a template literal can’t hold a symbol. The @ts-expect-error line proves a misspelled event won’t compile; it still runs, since types are gone at runtime.
keyof and strings: quick reference
| You need | Use | Result |
|---|---|---|
| Keys of an interface | keyof Customer | "id" | "name" | "city" |
| Only string keys | keyof T & string | Drops number and symbol |
| New key names | `get${Capitalize<K>}` | Template literal keys |
Keep going with these object guides:
- Check if a key exists in a TypeScript object
- Work with key-value pairs in TypeScript
- satisfies vs as in TypeScript
- TypeScript enums
Frequently asked questions
Why is keyof T string | number | symbol?
JavaScript keys can be strings, numbers or symbols, and a generic can’t rule any out.
How do I get only the string keys of a type?
Write keyof T & string or Extract<keyof T, string>.
How do I convert a string to keyof T?
Check it at runtime with a type guard: Object.hasOwn(obj, key) plus a key is keyof T return type. Prefer Object.hasOwn() over in, which also matches toString.
What does error TS7053 mean?
You indexed an object with a string it has no index signature for. Narrow the key first.
Where is keyof documented?
See Keyof Type Operator in the TypeScript Handbook.

Bijay Kumar is a 13-time Microsoft MVP with more than 18 years in software development, and the founder of Python Guides and TSinfo Technologies. He started out building .NET and SharePoint solutions at HP, TCS and KPIT before moving into Python, machine learning and AI, and he also builds web apps with TypeScript and React. He writes the tutorials here himself, and every example is run before publishing so you see the real output. More about Bijay · Microsoft MVP profile · LinkedIn