14.2 keyof: Getting the Keys of a Type
Right, let’s talk about keyof. It’s one of those TypeScript fundamentals that seems trivial until you realize it’s the secret ingredient in almost every powerful type operation you’ll ever do. It’s the crowbar that lets you pry open a type, extract its keys, and do something useful with them.
Think of any object type. It’s just a bag of properties, each with a name (a key) and a value. keyof is how you get a union type of all those keys. It’s like asking TypeScript, “Hey, for this type Person, what are the valid strings I could use to index into an object of this type?”
type Person = {
name: string;
age: number;
isProgrammer: boolean;
};
type PersonKeys = keyof Person;
// ^? type PersonKeys = "name" | "age" | "isProgrammer"
Now you have PersonKeys, which is a union of the literal string types "name", "age", and "isProgrammer". This is incredibly powerful because it’s locked to the shape of Person. If you add a new property to Person later, PersonKeys automatically gets updated. This is the foundation of keeping your types in sync without manual effort.
It Works on More Than Just Your Plain Objects
Don’t think keyof is only for your hand-crafted object types. It works on any type that has index signatures, including arrays, Record types, and even built-in types like Date.
// With a Record utility type
type MyRecord = Record<'a' | 'b', number>;
type KeysOfRecord = keyof MyRecord;
// ^? type KeysOfRecord = "a" | "b"
// With an array (or tuple)
type ArrayKeys = keyof ['zero', 'one', 'two'];
// ^? type ArrayKeys = number | "0" | "1" | "2" | "length" | ...
// Wait, what? That seems messy.
Ah, see? I told you there are rough edges. For array types, keyof gives you all the index numbers as string literals ("0", "1", etc.) plus all the array method and property names ("length", "push", "map", etc.). This is technically correct—you can index an array with "length"—but it’s often not what you want when you’re thinking about data keys. This is a classic case of the type system being a bit too literal for its own good. Usually, you’d use keyof T with your own object types, not arrays.
The keyof and typeof Tag Team
You’ll often see keyof paired with typeof when you’re working with an actual JavaScript value, not just a type. This is how you bridge the gap between the value-level world and the type-level world.
const config = {
host: "localhost",
port: 8080,
retry: true,
} as const; // `as const` makes it a literal type, which is key here
type ConfigKeys = keyof typeof config;
// ^? type ConfigKeys = "host" | "port" | "retry"
// Now you can create a function that only accepts these keys
function getConfigValue(key: ConfigKeys) {
return config[key];
}
getConfigValue("host"); // Works
getConfigValue("password"); // Error: Argument of type '"password"' is not assignable...
This pattern is indispensable for creating type-safe utilities around configuration objects, feature flags, or any other constant value structure you have lying around in your code.
The keyof any Conundrum
Here’s a fun one: keyof any. What does that even mean? It means “the type of all values that can be used as an index for an object.” In TypeScript’s default configuration, this resolves to string | number | symbol. Because, yes, you can use numbers (obj[0]) and symbols (obj[mySymbol]) as keys too, not just strings.
This is useful when you’re writing super generic code. For example, the type signature for Object.keys is keyof any because JavaScript objects can, in fact, have those types as keys.
// A very generic function that works on any object
function getKeys<T>(obj: T): (keyof T)[] {
return Object.keys(obj) as (keyof T)[];
}
Wait, why the as (keyof T)[] cast? Because of one of the biggest “gotchas” with keyof. Object.keys returns string[], not (keyof T)[]. This is a deliberate design choice for soundness reasons. Your object might have more specific keys than just string (like our Person type), but at runtime, Object.keys just returns strings. TypeScript can’t guarantee that every string in that array is actually a key of T, so it plays it safe. You, the knowledgeable developer, have to assert that type when you know it’s correct. It’s a small but important moment of friction between the type system and runtime reality.