Ch. 2 · TypeScript

TS7053: Element Implicitly Has an 'any' Type in TypeScript

Fix TS7053 'Element implicitly has an any type because expression of type string can't be used to index type' with keyof, Record and guards.

~7 min readintermediateupdated Oct 4, 2026

You looked up a value by a key held in a variable and the compiler stopped:

src/labels.ts(4,10): error TS7053: Element implicitly has an 'any' type because expression of type 'string' can't be used to index type '{ draft: string; published: string; archived: string; }'.
  No index signature with a parameter of type 'string' was found on type '{ draft: string; published: string; archived: string; }'.
Text

The object’s type has exactly three keys. Your key is typed string, which could be "draft" or "banana". TypeScript cannot pick a property type for an arbitrary string, so the result of the lookup would be any. With noImplicitAny (part of strict), an implicit any is an error, and this is how it is reported for element access.

Quick fix checklist

  • If the key is always one of the object’s keys, type the key as that union at its source: status: keyof typeof labels, or a named Status type.
  • If the key comes from user input, a URL or JSON, check it with a type guard (isStatus(value): value is Status) before indexing, and handle the miss.
  • If the object is a real dictionary with open-ended keys, type it Record<string, T> or use a Map, and expect undefined for missing keys.
  • Iterating with Object.keys or for...in gives string keys; use Object.entries when you need values.
  • In generic helpers, type the key parameter K extends keyof T instead of string.
  • Do not write obj[key as keyof typeof obj] unless the key was already validated.

Before you start

The error requires noImplicitAny, which strict: true enables. Without it, the same code compiles and the lookup is silently any, which is worse. The old escape hatch suppressImplicitAnyIndexErrors was deprecated in TypeScript 5.0 and is gone: 5.9 reports it as removed and 7.0 as an unknown option. Examples here were compiled with TypeScript 7.0 and 5.9 using strict: true; the TS7053 text is the same in both.

You should be comfortable with keyof (the union of an object type’s keys) and typeof in a type position (the type of a value).

Why it happens

An element access obj[k] type-checks when k’s type is assignable to keyof the object’s type, or when the type has an index signature that accepts k. Object literals get neither: { draft: "Draft" } is inferred as { draft: string }, whose keyof is "draft", with no [key: string] signature. A string is wider than "draft" | "published" | "archived", so neither rule applies. The second line of the message is the compiler listing the index signature it looked for and didn’t find.

Two places produce string keys even though they look safe:

  • Object.keys(obj) returns string[], and a for...in variable is string. This is deliberate. TypeScript’s types are structural, so a value of type { draft: string } may be an object with many more properties at runtime. Claiming the keys are exactly keyof T would be a lie the compiler can’t verify.
  • Narrowing with if (status in labels) narrows labels, not status. Inside the branch, status is still string, and the lookup still fails.

The same check reports expression of type 'number' when you index an object literal with a number, for the same reason.

Step-by-step walkthrough

Step 1: Reproduce and identify where the key comes from

const labels = { draft: "Draft", published: "Published", archived: "Archived" };

export function label(status: string) {
  return labels[status];
}
TypeScript

The lookup is on line 4, but the decision lives in the signature on line 3: status: string. Ask who calls label and with what. If every caller passes a value from your own code, the type is just too wide. If a caller passes req.query.status, the key is untrusted and needs checking.

Step 2: Type the key as a union when it is known

Declare the set of keys once and derive both the object type and the key type from it:

const STATUSES = ["draft", "published", "archived"] as const;
type Status = (typeof STATUSES)[number];

const labels: Record<Status, string> = {
  draft: "Draft",
  published: "Published",
  archived: "Archived",
};

export function labelFor(status: Status) {
  return labels[status];
}
TypeScript

as const keeps the array elements as literal types instead of widening them to string, so (typeof STATUSES)[number] is "draft" | "published" | "archived". Record<Status, string> makes the compiler require a label for every status, so adding a fourth status to the array without a label becomes a compile error.

Step 3: Validate untrusted keys with a type guard

function isStatus(value: string): value is Status {
  return (STATUSES as readonly string[]).includes(value);
}

export function labelFromQuery(raw: string) {
  return isStatus(raw) ? labels[raw] : "Unknown";
}
TypeScript

The widening cast inside the guard is needed because STATUSES.includes(value) on a readonly tuple of literals rejects a string argument (Argument of type 'string' is not assignable to parameter of type '"draft" | "published" | "archived"'). Widening the array to readonly string[] for the check is safe; the guard then narrows the key for the caller.

Step 4: Use a dictionary type when keys really are open

const counts: Record<string, number> = {};

export function bump(word: string) {
  counts[word] = (counts[word] ?? 0) + 1;
}
TypeScript

A Record<string, number> (equivalent to { [key: string]: number }) accepts any string key. The catch: reads are typed number even for missing keys. Turn on noUncheckedIndexedAccess so reads become number | undefined, or use a Map<string, number>, whose get already returns number | undefined.

Step 5: Fix iteration and generic helpers

for (const [key, text] of Object.entries(labels)) {
  console.log(key, text);
}

export function get<T extends object, K extends keyof T>(obj: T, key: K): T[K] {
  return obj[key];
}
TypeScript

Object.entries gives you the value without indexing, so there is nothing to type. In the generic helper, K extends keyof T makes the return type precise (get(labels, "draft") is string) and rejects get(labels, "deleted") at the call site.

Worked scenario

A product listing reads the sort order from the URL:

interface Product { name: string; price: number; rating: number }

const sorters = {
  price: (a: Product, b: Product) => a.price - b.price,
  name: (a: Product, b: Product) => a.name.localeCompare(b.name),
  rating: (a: Product, b: Product) => b.rating - a.rating,
};

export function sortProducts(products: Product[], search: string) {
  const sort = new URLSearchParams(search).get("sort") ?? "price";
  return [...products].sort(sorters[sort]);
}
TypeScript
src/sort.ts(11,29): error TS7053: Element implicitly has an 'any' type because expression of type 'string' can't be used to index type '{ price: (a: Product, b: Product) => number; name: (a: Product, b: Product) => number; rating: (a: Product, b: Product) => number; }'.
Text

Diagnosis: sort is whatever the visitor typed into the address bar. The compiler is pointing at a real bug: ?sort=popularity would pass undefined to Array.prototype.sort, which silently falls back to sorting by string conversion. Validate the key and fall back explicitly:

const sorters = {
  price: (a: Product, b: Product) => a.price - b.price,
  name: (a: Product, b: Product) => a.name.localeCompare(b.name),
  rating: (a: Product, b: Product) => b.rating - a.rating,
} satisfies Record<string, (a: Product, b: Product) => number>;

export type SortKey = keyof typeof sorters;

export function isSortKey(value: string): value is SortKey {
  return Object.hasOwn(sorters, value);
}

export function sortProducts(products: Product[], search: string) {
  const requested = new URLSearchParams(search).get("sort");
  const key: SortKey = requested !== null && isSortKey(requested) ? requested : "price";
  return [...products].sort(sorters[key]);
}
TypeScript

satisfies checks every entry is a comparator while keeping the specific keys, so SortKey is "price" | "name" | "rating". Object.hasOwn only accepts the object’s own properties.

Common mistake

The fix that appears in most search results is the cast: sorters[sort as keyof typeof sorters]. It compiles, and the result is typed as a comparator, but nothing checked the value. For ?sort=popularity you get undefined at runtime with a type that says it can’t happen.

A subtler mistake is guarding with in: if (sort in sorters). Besides not narrowing the key, in walks the prototype chain, so "toString" in sorters is true, and ?sort=toString would pass Object.prototype.toString to sort. Use Object.hasOwn in the guard.

Finally, adding [key: string]: Comparator to the object’s type silences TS7053 everywhere, including real typos such as sorters.prise.

Verify the behavior

npx tsc --noEmit should exit with status 0. Then test the inputs the compiler warned you about, including an inherited key:

import { test } from "node:test";
import assert from "node:assert/strict";
import { sortProducts, type Product } from "./sort.ts";

const products: Product[] = [
  { name: "Lamp", price: 40, rating: 4.1 },
  { name: "Desk", price: 120, rating: 4.8 },
  { name: "Chair", price: 85, rating: 3.9 },
];

test("sorts by a valid key from the query string", () => {
  assert.deepEqual(sortProducts(products, "?sort=name").map((p) => p.name), ["Chair", "Desk", "Lamp"]);
});

test("falls back to price for unknown or inherited keys", () => {
  for (const search of ["?sort=popularity", "?sort=toString", ""]) {
    assert.deepEqual(sortProducts(products, search).map((p) => p.price), [40, 85, 120]);
  }
});
TypeScript

node --test src/sort.test.ts should report pass 2 and fail 0.

Interview exercise

Why does Object.keys(user) return string[] instead of (keyof User)[], and how do you iterate over an object’s properties type-safely?

Answer and reasoning

TypeScript’s object types describe a minimum shape, not an exact one. A function that accepts { id: string; name: string } can be called with an object that also has password and createdAt, because extra properties are allowed by structural typing. At runtime Object.keys returns all of them. If its return type were (keyof User)[], then user[key] would be typed string while actually reading createdAt, a Date. Returning string[] is the honest type.

To iterate safely, use Object.entries when you need the values, or iterate over a declared list of keys (const FIELDS = ["id", "name"] as const) when you need to touch specific properties. Casting Object.keys(user) as (keyof User)[] is acceptable only when you control how the object was created, for example a literal in the same module. A strong answer connects this to TS7053: both come from the compiler refusing to assume a string is one of a known set of keys.

Continue learning

More in TypeScript

esc