TypeScript is great at catching mistakes before they reach your users, but sometimes, it catches things that don't feel like mistakes at all. You write what looks like perfectly reasonable code, and TypeScript pushes back with an error about types being too broad. One of the most common places this happens is when you try using a variable as an index in an object.
While building a client invoice web application, I ran into this problem. I had a config object mapping invoice statuses to Tailwind CSS classes, and I wanted to look up the right styles based on a status prop but TypeScript refused. The fix turned out to be two small keywords: keyof typeof.
In this article, I will explain what the problem was, what those keywords do, and why this pattern is worth reaching for whenever you're working with objects in TypeScript with a specific object structure.
The Problem
I have a status badge component and a config object mapping statuses to Tailwind classes:
const statusConfig = {
Paid: {
dot: "bg-emerald-400",
bg: "bg-emerald-50",
text: "text-emerald-600",
},
Pending: {
dot: "bg-amber-400",
bg: "bg-amber-50",
text: "text-amber-600",
},
Draft: {
dot: "bg-slate-400",
bg: "bg-slate-100",
text: "text-slate-500",
},
};
export default function App() {
function StatusBadge({ status }: { status: string }) {
const cfg = statusConfig[status];
return cfg;
}
console.log(StatusBadge("Pending")); // TypeScript shows an error
return (
<h1>Content goes here</h1>
)
}
This is the error TypeScript throws:
statushas an implicit type of 'any' because expression of type 'string' can't be used to index type '{ Paid: ...; Pending: ...; Draft: ...; }'
The issue is that status is typed as a string which is too broad. TypeScript can't guarantee that an arbitrary string will match any of the 3 keys in the statusConfig object. The fix is to use the keyof typeof type in TypeScript.
keyof typeof
The keyof typeof operator extracts the keys of an object as a union type
keyof typeof statusConfig;
// Equivalent to: "Paid" | "Pending" | "Draft"
- typeof statusConfig – This tells TypeScript to get the type of the data in question, which in this case is an object
- keyof – This tells TypeScript to extract all the keys from the object as union types. It will convert the keys in the object into a set of possible options. This way, TypeScript has an idea of the shape of the statusConfig object.
Applying this in the StatusBadge component:
function StatusBadge({ status }: { status: keyof typeof statusConfig }) {
const cfg = statusConfig[status]; // It now works perfectly
return cfg;
}
You could also save this as a type variable to make the code more readable:
type statusType = keyof typeof statusConfig;
function StatusBadge({ status }: { status: statusType }) {
const cfg = statusConfig[status];
return cfg;
}
Now TypeScript knows that status can only ever be "Paid", "Pending", or "Draft", so indexing into statusConfig is safe.
Why Not Just Use a Type Alias?
You could manually define a type like this:
type Status = "Paid" | "Pending" | "Draft";
And just use that directly, but this will create a maintenance burden. If you were to add or rename a key in statusConfig, you have to update the type separately as well. With keyof typeof, the type is derived directly from the object, so it always in sync as the keys are generated automatically for you, making development faster and less error prone.
keyof typeof is the right tool whenever you need to accept a dynamic value for an object, and you are not sure what that value is. It's a small trick but makes TypeScript work with your data instead of fighting against it.
Conclusion
Type errors like this one can feel like TypeScript getting in your way, but they're usually pointing at something real. In this case, that a plain string is too vague to safely index a specific object. keyof typeof gives you a way to be precise without writing out types by hand, keeping your code both safe and easy to maintain.
Top comments (1)
Really clean explanation — keyof typeof is one of those TypeScript patterns that feels “magical” until you break it down into the two separate inference steps.
What I like most about this pattern is how it enables single-source-of-truth typing. Instead of duplicating keys in unions or interfaces, you let the runtime object define the contract, and TypeScript derives everything from it.
One subtle extension that often comes up in real codebases is combining this with as const to tighten inference even further:
typeof obj gives structure
keyof typeof obj gives safe key unions
as const prevents widening so keys stay literal instead of string
That trio is usually what makes configs, maps, and registries fully type-safe without extra boilerplate.
Also interesting edge case: once objects become dynamic (fetched/config-driven), this pattern starts losing safety guarantees, so teams often shift toward schema validation (Zod / Valibot) to reintroduce runtime guarantees.
Curious if you’ve found this pattern more useful in utility functions, or in larger “config object as API surface” designs?