keyof and typeof in TypeScript: Deriving Types from Values and Keys
We derive types from values with type-level typeof, build unions from a type's keys with keyof, look at the index signature surprise, and use the keyof typeof pattern in practice.
Version note: The code in this post targets TypeScript 7.0 and Node 26.
Predict First
This time we write no types by hand; all we have is an ordinary object:
const config = { host: "localhost", port: 8080, secure: false };
type Config = typeof config;
type ConfigKey = keyof Config;
const key: ConfigKey = "hostname";
Two questions: Which type does ConfigKey correspond to? Does the last line compile? The answer is in the section where we see what keyof produces.
Two Different typeofs
You know the typeof operator from the JavaScript series: at runtime it returns the kind of a value as a string. TypeScript uses the same word for a second job. The difference is where the word is written:
const port = 8080;
let retries = 3;
console.log(typeof port); // number
type Port = typeof port; // 8080
type Retries = typeof retries; // number
The typeof inside console.log is part of a value expression. It's evaluated when the code runs and produces the string "number". The typeof on the type Port = … line sits inside a type expression. It never exists at runtime; it tells the compiler "take the type of this variable." Since port is a const, its type is the literal type 8080 we saw earlier, while retries is a let, so it's number.
You can think of these as two separate worlds: the world of values and the world of types. const, let and functions live in the value world; type and interface live in the type world. Type-level typeof is a door from the value world into the type world.
Deriving a Type from a Value with typeof
Where this door really pays off is with things whose type is already written down in a value. Instead of writing the same structure once as an object and again as an interface, you make the value the single source of truth:
const defaultSettings = {
theme: "dark",
fontSize: 14,
notifications: { email: true, push: false },
};
type Settings = typeof defaultSettings;
function applySettings(settings: Settings): void {
console.log(settings.theme, settings.notifications.push);
}
applySettings({ theme: "light", fontSize: 16, notifications: { email: false, push: true } });
Settings carries the whole structure of defaultSettings, nested objects included: { theme: string; fontSize: number; notifications: { email: boolean; push: boolean; }; }. If you add a new property to defaultSettings tomorrow, Settings updates on its own.
typeof works with functions too, and gives you the type of the whole function:
function createUser(name: string, age: number) {
return { id: Date.now(), name, age };
}
type CreateUser = typeof createUser;
// (name: string, age: number) => { id: number; name: string; age: number; }
const createGuest: CreateUser = (name, age) => ({ id: 0, name, age });
Let's also look at how it combines with as const. As we saw earlier, as const infers values as literal and readonly, and typeof captures that narrow type as is:
const theme = { mode: "dark", accent: "teal" } as const;
type Theme = typeof theme;
// { readonly mode: "dark"; readonly accent: "teal"; }
keyof: A Union of a Type's Keys
We briefly met keyof when we talked about generic constraints. Now let's take a closer look. keyof takes an object type and produces a union of that type's keys:
interface User {
id: string;
name: string;
email: string;
}
type UserKey = keyof User; // "id" | "name" | "email"
const sortBy: UserKey = "name";
// Error: Type '"phone"' is not assignable to type 'keyof User'.
const filterBy: UserKey = "phone";
When you hover over UserKey in your editor, you'll usually see the text keyof User rather than an expanded union. The error message likewise says 'keyof User'. That's not a shortcoming; TypeScript prefers to show a type the way you defined it. It still means "id" | "name" | "email".
Now we can answer the opening question. typeof config captures the object's type as { host: string; port: number; secure: boolean; }. keyof pulls the keys out of that, and ConfigKey becomes "host" | "port" | "secure". That's why the last line doesn't compile:
error TS2322: Type '"hostname"' is not assignable to type '"host" | "port" | "secure"'.
The nice part is that we never wrote this union by hand. When a new key is added to config, ConfigKey includes it automatically. The risk of keeping the key list in a second place and forgetting to update it disappears.
keyof and Index Signatures
The result of keyof isn't always a literal union. With a type that has an index signature, which we saw earlier, things are different:
type Scores = { [player: string]: number };
type ScoreKey = keyof Scores; // string | number
const a: ScoreKey = "Ada";
const b: ScoreKey = 7;
// Error: Type 'boolean' is not assignable to type 'string | number'.
const c: ScoreKey = true;
Why string | number rather than just string? Because in JavaScript object keys are always converted to strings; writing scores[7] reads the same property as scores["7"]. TypeScript reflects that behavior in the type: an index signature with string keys accepts number keys as well. Arrays hold a similar surprise: keyof string[] isn't just number; it also includes the names of every method and property, such as "length", "push" and "map".
keyof typeof: The Two Together
The pattern you'll use most often is writing the two side by side. If you want a union of keys from a value, you first get its type with typeof, then its keys with keyof:
const statusLabels = {
draft: "Draft",
published: "Published",
archived: "Archived",
} as const;
type Status = keyof typeof statusLabels; // "draft" | "published" | "archived"
function getLabel(status: Status): string {
return statusLabels[status];
}
console.log(getLabel("published")); // Published
// Error: Argument of type '"deleted"' is not assignable to parameter of type '"archived" | "draft" | "published"'.
getLabel("deleted");
Read keyof typeof statusLabels from right to left: first typeof statusLabels gives the object's type, then keyof gives that type's keys. In the end we have both an object used at runtime and a union type derived from it, and the two can never drift apart. To add a new status, adding one line to the object is enough. This pattern will come back later as an erasable alternative to TypeScript's own enums.
keyof in Generic Constraints
The power of keyof shows when it's combined with generics. Let's write a function that collects the values of a given key from an array of objects:
function pluck<T, K extends keyof T>(items: T[], key: K) {
return items.map((item) => item[key]);
}
const people = [
{ name: "Ada", age: 36 },
{ name: "Grace", age: 45 },
];
const names = pluck(people, "name"); // string[]
const ages = pluck(people, "age"); // number[]
console.log(names, ages); // [ 'Ada', 'Grace' ] [ 36, 45 ]
K extends keyof T requires the second argument to be one of T's keys; write pluck(people, "email") and you get an error while you're still typing. We didn't write the return type; TypeScript inferred it, finding string[] for "name" and number[] for "age". But what if we wanted to write that return type ourselves? There's a way to say "the type of T's property at key K," and it's the topic of the next post.
Common Pitfalls
The first pitfall is expecting Object.keys to return keyof:
const config = { host: "localhost", port: 8080, secure: false };
for (const key of Object.keys(config)) {
// Error: Element implicitly has an 'any' type because expression of type 'string' can't be used to index type '{ host: string; port: number; secure: boolean; }'.
console.log(config[key]);
}
Object.keys always returns string[], not ("host" | "port" | "secure")[]. That's not an oversight; it's a deliberate decision. Because of structural typing, an object can carry more properties than its type lists; there's no guarantee that the keys Object.keys returns at runtime will be only the ones in the type. If you're sure about the full list of keys, you can write a type assertion like Object.keys(config) as (keyof typeof config)[], but know that you're making a promise.
The second pitfall is mixing up the two worlds. Write a value where a type is expected and you get 'config' refers to a value, but is being used as a type here. Did you mean 'typeof config'?; the message itself tells you the fix. The reverse holds too: if you try to use an interface as a value, as in console.log(User), you get 'User' only refers to a type, but is being used as a value here., because interfaces don't exist at runtime.
The third pitfall is trying to apply typeof to the result of a function call. Writing type NewUser = typeof createUser("Ada", 36); is a syntax error (';' expected.); type-level typeof only works with variable and property names. We'll see the right way to get a function's return type in the post on utility types.
A Small Experiment
★☆☆ Write let mode = "dark"; and const fixedMode = "dark";. Predict the types typeof mode and typeof fixedMode, then check them in your editor.
★★☆ Remove the as const from the statusLabels example. Does the Status type change? What about typeof statusLabels? Compare the two by hovering over them in your editor, and say whether as const affects the keys or the values.
★★★ Predict whether these compile: const key: keyof string[] = "length";, const index: keyof string[] = 0; and const big: keyof string[] = 5n;. Once you've found which one errors, explain why keyof produces such a wide union for arrays.
In this post we learned to get a type's keys with keyof. The next step is getting the types behind those keys. With indexed access types we'll see how to reach a single property's type with User["email"], how to extract an array's element type with T[number], and how to write pluck's return type ourselves.