keyof, typeof & Indexed Access Types

advanced keyof typeof indexed-access advanced-types

These three are the building blocks of every fancy TypeScript pattern we’ll write. In simple language: keyof gives us the keys of a type, typeof gives us the type of a value, and indexed access (T[K]) gives us the type of a property. Combine them and we can describe almost anything.

keyof — grab the keys as a union

keyof T returns a union of all the property names of T as string (or number) literals. Think of it like Object.keys() but at the type level.

type User = { id: number; name: string; email: string };

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

function getField(user: User, key: keyof User) {
  return user[key]; // safe — key is one of the real keys
}

The big win is that keyof follows the type around. If we add a field to User, UserKey updates automatically — no manual sync.

typeof — promote a value into a type

typeof (the TS one, not the JS one) takes a runtime value and asks “what is the type of this thing?”. Super useful when we have a config object or constant and want to derive a type from it instead of typing it twice.

const config = {
  apiUrl: "https://api.example.com",
  timeout: 5000,
  retries: 3,
};

type Config = typeof config;
// { apiUrl: string; timeout: number; retries: number }

The only difference from keyof is that typeof operates on values, while keyof operates on types. Chain them and we get the killer combo: keyof typeof config.

Indexed access — drill into a property’s type

T[K] reads the type of property K from type T. Just like bracket access in JS, except at the type level.

type User = { id: number; name: string; address: { city: string } };

type IdType = User["id"];           // number
type CityType = User["address"]["city"]; // string — we can nest
type IdOrName = User["id" | "name"];     // number | string — union of keys works too
type User = { id: number; name: string }
keyof User
"id" | "name"
User["id"]
number
User[keyof User]
number | string

The killer combo

The real power shows up when we chain all three. Here’s a type-safe pick function — the kind of thing interviewers love.

function pluck<T, K extends keyof T>(items: T[], key: K): T[K][] {
  return items.map((item) => item[key]);
}

const users = [{ id: 1, name: "Ada" }, { id: 2, name: "Linus" }];
const names = pluck(users, "name"); // string[]
const ids = pluck(users, "id");     // number[]
// pluck(users, "email");          // error — "email" not in keyof T

K extends keyof T constrains K to actual keys of T. The return type T[K][] uses indexed access to figure out what each value is. We get full type safety with zero duplication.

Arrays and number indexing

For arrays and tuples, indexed access with number gives the element type. Handy when we want to pull an element out of an array type.

const roles = ["admin", "editor", "viewer"] as const;
type Role = typeof roles[number]; // "admin" | "editor" | "viewer"

We just turned a runtime array into a union type. This pattern shows up everywhere — defining allowed values once and reusing them as both data and type.