TypeScript keyof and Strings: keyof T & string, Guards and TS7053

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(...)'.
Command Prompt output of tsc errors TS2322, TS2339 and TS2731 showing keyof is string | number | symbol
TS2322 reveals 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')
Command Prompt output of a TypeScript type guard with Object.hasOwn converting strings to keyof typeof prices
Real plans print a price, unknown strings are rejected, and the cast crashes.

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; }'.
Command Prompt output of tsc error TS7053 element implicitly has an any type because expression of type string can't be used to index
Both the function and the 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
Command Prompt output of TypeScript template literal types built from keyof Address with event names and getters
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 needUseResult
Keys of an interfacekeyof Customer"id" | "name" | "city"
Only string keyskeyof T & stringDrops number and symbol
New key names`get${Capitalize<K>}`Template literal keys

Keep going with these object guides:

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.