keyof, typeof & Lookup Types

Reviewed & published by Brayan K

keyof T produces a union of a type's property names, the typeof type operator derives a type from an existing value, and an indexed access (lookup) type T[K] reads the type of one of T's properties — together they let you write accessors that are checked against the real shape of your data.

Part of the free TypeScript course at LearnCodingFast — hands-on lessons with worked examples and the output they print, plus practice exercises and a quick quiz.

What You'll Learn

💡 Real-World Analogy

Picture a filing cabinet (your object type). keyof is the printed list of drawer labels — every name you're allowed to ask for. The typeof type operator is taking a photograph of an existing cabinet and turning it into a reusable blueprint, so a new cabinet can be built to match. T[K] is opening one specific drawer to see what kind of thing lives in it. Ask for a label that isn't on the list and the clerk (the compiler) stops you before you waste time opening a drawer that doesn't exist.

1. keyof — Property Names as a Union

keyof T collects every property name of T into a union of string (and number/symbol) literal types. It's the type-level version of "what keys does this object have?" — and it stays in sync automatically when you add or rename a property:

interface User { id: number; name: string; email: string }

type UserKeys = keyof User;     // "id" | "name" | "email"

let k: UserKeys = "name";       // ✅ allowed
k = "email";                    // ✅ allowed
// k = "phone";                 // ❌ not assignable to keyof User

// keyof on a Record gives its key type:
type Scores = Record<string, number>;
type ScoreKey = keyof Scores;   // string
// === keyof — the union of an object type's property NAMES ===
// In TS:  type UserKeys = keyof User;  // "id" | "name" | "email"
// keyof is type-level, but the runtime data it describes is the set of keys:
const user = { id: 1, name: "Alice", email: "alice@example.com" };
const keys = Object.keys(user);          // the runtime form of "keyof User"
console.log("keys:", keys);              // keys: [ 'id', 'name', 'email' ]
console.log("how many:", keys.length);   // how many: 3

// You can only safely read a key that's actually present.
// "keyof" guarantees that at the type level; here we check it at runtime:
function hasKey(obj, key) {
  return Object.prototype.hasOwnProperty.call(obj, key);
}
console.log("has name?", hasKey(user, "name"));   // has name? true
console.log("has nope?", hasKey(user, "nope"));   // has nope? false

// Iterating every key — the runtime of "for each K in keyof User":
for (const k of keys) {
  console.log(k + " =", user[k]);
}
// id = 1
// name = Alice
// email = alice@example.com

2. The typeof Type Operator & T[K]

TypeScript reuses the typeof keyword in type positions to mean "the type of this value". It's perfect for deriving a type from a config object or a constant you already have. Pair it with an indexed access type T[K] to read individual property types, or T[keyof T] for the union of all value types:

const config = { host: "localhost", port: 8080, secure: false };

// typeof TYPE operator — derive a type from the value
type Config = typeof config;
// { host: string; port: number; secure: boolean }

// Indexed access (lookup) types read a property's TYPE
type Port = Config["port"];         // number
type HostOrPort = Config["host" | "port"];  // string | number

// T[keyof T] — the union of ALL value types
type ConfigValues = Config[keyof Config];   // string | number | boolean

// Common combo: a union of literal keys from a const
const Roles = { admin: "admin", user: "user" } as const;
type Role = typeof Roles[keyof typeof Roles];  // "admin" | "user"
// === The typeof TYPE operator + indexed access T[K] ===
// In TS there are TWO typeofs:
//   - the JS runtime  typeof x  -> a string like "object"
//   - the TS TYPE operator  typeof x  -> the inferred TYPE of x
// We can only RUN the JavaScript one, so let's see it, then read about the other.
const config = { host: "localhost", port: 8080, secure: false };

// Runtime typeof (JavaScript): a string describing the value's kind
console.log(typeof config);          // object
console.log(typeof config.host);     // string
console.log(typeof config.port);     // number
console.log(typeof config.secure);   // boolean

// TS TYPE operator (read-only idea):
//   type Config = typeof config;
//   // { host: string; port: number; secure: boolean }
// It lets you derive a TYPE from an existing VALUE — great for config objects.

// Indexed access T[K] reads the TYPE of a property. At runtime the analogue
// is reading the VALUE at that key:
const portValue = config["port"];    // TS type would be Config["port"] = number
console.log("port value:", portValue);   // port value: 8080

// T[keyof T] is the union of all VALUE types. Runtime analogue: all values.
console.log("all values:", Object.values(config));
// all values: [ 'localhost', 8080, false ]

3. keyof + Generics: a Type-Safe Getter

Put it all together: a generic <T, K extends keyof T> getter constrains the key to a real property and uses the lookup type T[K] to return its exact type. This is the single most common real-world use of keyof — and it generalises to picking several keys at once:

// One key — return its precise type
function get<T, K extends keyof T>(obj: T, key: K): T[K] {
  return obj[key];
}
const product = { sku: "A100", title: "Mug", price: 9.5 };
get(product, "price");   // typed number -> 9.5
get(product, "nope");    // ❌ '"nope"' is not assignable to keyof product

// Several keys — return a Pick of just those
function pick<T, K extends keyof T>(obj: T, keys: K[]): Pick<T, K> {
  const out = {} as Pick<T, K>;
  for (const k of keys) out[k] = obj[k];
  return out;
}
pick(product, ["sku", "title"]);   // { sku: string; title: string }
// === Combining keyof + generics: a type-safe property getter ===
// In TS:  function get<T, K extends keyof T>(obj: T, key: K): T[K] { ... }
// keyof + a generic K constrains the key to a REAL key, and T[K] gives the
// precise property type. At runtime it's a safe indexed read:
function get(obj, key) {
  return obj[key];
}
const product = { sku: "A100", title: "Mug", price: 9.5, inStock: true };
console.log(get(product, "title"));   // Mug
console.log(get(product, "price"));   // 9.5
console.log(get(product, "inStock")); // true

// Pick several keys at once — every key is checked against keyof T.
// In TS:  function pick<T, K extends keyof T>(obj: T, keys: K[]): Pick<T, K>
function pick(obj, keys) {
  const out = {};
  for (const k of keys) out[k] = obj[k];
  return out;
}
const summary = pick(product, ["sku", "title"]);
console.log("summary:", summary);   // summary: { sku: 'A100', title: 'Mug' }

// Using a derived "typeof" type in TS keeps the getter in sync:
//   type Product = typeof product;
//   get<Product, "price">(product, "price");  // number
console.log("done");                // done

🎯 Your Turn

Implement a small getter whose key would be constrained by keyof T, and log the runtime form of keyof typeof settings. Fill in the blanks and match the expected output.

// 🎯 YOUR TURN — replace each ___ then press "Try it Yourself".
// Mirror a keyof getter:  function readSetting<T, K extends keyof T>(obj, key): T[K]

const settings = { theme: "dark", fontSize: 14, autosave: true };

function readSetting(obj, key) {
  // 1) Return the property at the given key (a safe indexed read)
  return ___;                  // 👉 obj[key]
}

console.log(readSetting(settings, "theme"));    // dark
console.log(readSetting(settings, "fontSize")); // 14

// 2) Log the runtime form of "keyof typeof settings"
console.log("keys:", ___);     // 👉 Object.keys(settings)

// ✅ Expected output:
//    dark
//    14
//    keys: [ 'theme', 'fontSize', 'autosave' ]

Common Errors (and the fix)

Pro Tips

Mini-Challenge: groupBy

Build a groupBy whose key is the runtime form of a keyof T constraint — bucket an array of objects by one property. Follow the outline, run it, and match the example output.

// 🎯 MINI-CHALLENGE: a type-safe "groupBy"
// In TS the key is constrained with keyof so it must be a real property:
//   function groupBy<T, K extends keyof T>(items: T[], key: K): Record<...> { ... }
//
// Write groupBy(items, key) that:
//   1. Buckets the items into an object keyed by item[key].
//   2. Each bucket is an array of the items that share that value.
//
// Test data:
//   const pets = [
//     { name: "Rex",   kind: "dog" },
//     { name: "Milo",  kind: "cat" },
//     { name: "Bella", kind: "dog" },
//   ];
//   const byKind = groupBy(pets, "kind");
//
// ✅ Example output:
//    dog count: 2
//    cat count: 1
//    groups: [ 'dog', 'cat' ]

// your code here

🎉 Lesson Complete

Practice quiz

What does keyof T produce?

  • An array of the object's values
  • The number of properties
  • A union of T's property names
  • A new object

Answer: A union of T's property names. keyof T is a union of the property names of T, e.g. keyof User is "id" | "name" | "email".

Is keyof a runtime feature like Object.keys?

  • No, keyof is a type operator and is erased at compile time
  • Yes, they are identical
  • keyof runs only in strict mode
  • keyof returns a Promise

Answer: No, keyof is a type operator and is erased at compile time. keyof is purely type-level; Object.keys is its runtime counterpart but they are not the same.

In a TYPE position, what does typeof x mean?

  • A runtime string like 'object'
  • The length of x
  • Always any
  • The inferred type of the value x

Answer: The inferred type of the value x. In a type position, typeof x is TypeScript's type operator: the inferred type of value x.

What does the indexed access (lookup) type T[K] read?

  • The value at runtime
  • The type of property K out of type T
  • The number of keys
  • A random property

Answer: The type of property K out of type T. T[K] reads the type of property K from T, e.g. User['name'] is string.

What is T[keyof T]?

  • The union of all of T's property value types
  • The first property's type
  • Always string
  • The keys as an array

Answer: The union of all of T's property value types. T[keyof T] is the union of every property value type in T.

Why constrain a key with <K extends keyof T> instead of typing it as string?

  • string is faster
  • It allows any key
  • It forces the key to be a real property name, turning typos into compile errors
  • It disables inference

Answer: It forces the key to be a real property name, turning typos into compile errors. Constraining to keyof T rejects typos like obj['naem'] and gives a precise T[K] return type.

How do you take the keys of a VALUE rather than a type?

  • keyof value
  • keyof typeof value
  • value.keyof
  • typeof keyof value

Answer: keyof typeof value. keyof operates on a type, so use keyof typeof value to first turn the value into a type.

What is keyof Record<string, number>?

  • number
  • string | number
  • never
  • string

Answer: string. Verified with tsc: keyof Record<string, number> is string, the record's key type.

For const Roles = { admin: "admin", user: "user" } as const, what is typeof Roles[keyof typeof Roles]?

  • string
  • "admin" | "user"
  • Roles
  • never

Answer: "admin" | "user". On an as const object this idiom produces the union of literal values "admin" | "user".

Why does writing { role: "admin" } without as const widen role to string?

  • A bug in TypeScript
  • Strings are always any
  • Mutable object properties are widened from their literal type
  • Because role is optional

Answer: Mutable object properties are widened from their literal type. Without as const the literal "admin" widens to string; as const keeps the literal type.

Continue this course

Frequently asked questions

Is keyof a runtime feature like Object.keys?

No. keyof is purely a TYPE operator — it produces a union of an object type's property names at compile time and is erased before the code runs. Object.keys is the runtime counterpart that returns an actual array of the object's own enumerable string keys. They look related and often line up, but they aren't the same: keyof can include keys you never enumerate (like number or symbol keys in a type), while Object.keys reflects exactly what exists on the value at runtime, as strings.

There are two typeofs — how do I tell them apart?

Context. In a VALUE position (an expression), typeof x is JavaScript's runtime operator and evaluates to a string like 'object' or 'number'. In a TYPE position (after a colon, in a type alias, or inside <>), typeof x is TypeScript's type operator and means 'the inferred type of the value x'. For example, const c = { port: 80 }; type C = typeof c gives the type { port: number }. The same keyword does different jobs depending on whether it appears where a value or a type is expected.

What is an indexed access (lookup) type T[K]?

T[K] reads the TYPE of property K out of type T, mirroring how obj[key] reads a value at runtime. If User is { id: number; name: string }, then User['name'] is string and User['id'] is number. You can pass a union of keys too: User['id' | 'name'] is number | string. The special case User[keyof User] gives the union of ALL property value types. Lookup types are what let a generic getter return the exact type of whatever property you ask for.

Why combine keyof with a generic instead of typing the key as string?

Typing the key as a plain string accepts any string, so a typo like obj['naem'] compiles and silently returns undefined. Constraining the key with <K extends keyof T> forces it to be one of T's actual property names, turning the typo into a compile error, and the return type T[K] is the precise property type rather than a vague any. That combination — keyof for safety on the key, the lookup T[K] for precision on the result — is the standard recipe for any type-safe accessor.