DevgainsDevgainsDevgains
All articles

keyof, typeof & Indexed Access Types: TypeScript's Type-Level Toolkit

·8 min read

The keyof operator, the typeof type operator, and indexed access types are the three tools that let you derive TypeScript types from values and other types instead of hand-writing them twice. Learn them together and a whole class of "the type drifted out of sync with the object" bugs disappears — because the types are computed from the source of truth, not maintained in parallel. This guide sits in the Devgains TypeScript cluster next to generics and as const, which are the two features these operators pair with most often.

Quick answer: what do these three operators do?

  • keyof T takes a type and produces a union of its property keys as string (or number/symbol) literal types.
  • typeof value (in a type position) takes a runtime value and produces its type.
  • T[K] — an indexed access type — takes a type T and a key K and produces the type of that property.

Chained together they read like a sentence: "the type of the values in this object" is (typeof obj)[keyof typeof obj]. That one expression is the backbone of type-safe config objects, enums-from-objects, and lookup tables.

Why derive types instead of writing them?

Every time you write a type by hand that mirrors a value, you create two things that must be kept in sync manually. Add a key to the object, forget to update the type, and you have a silent bug the compiler can't catch because you told it the wrong shape.

Deriving types inverts that. The value is the single source of truth, and the type is computed from it. Add a key and every derived type updates automatically. This is the same DRY instinct behind type-safe API calls without a code generator — push the compiler to infer structure rather than restate it.

keyof: turning an object type into a union of keys

keyof reads the keys of a type and gives you their union:

interface User {
  id: number;
  name: string;
  email: string;
}
 
type UserKey = keyof User; // "id" | "name" | "email"

That union is the raw material for safe property access. A generic "get a property" helper that only accepts real keys looks like this:

function getProp<T, K extends keyof T>(obj: T, key: K): T[K] {
  return obj[key];
}
 
const user: User = { id: 1, name: "Ada", email: "[email protected]" };
 
const name = getProp(user, "name"); // typed as string
const id = getProp(user, "id");     // typed as number
getProp(user, "phone");             // compile error: "phone" is not a key of User

Notice the return type T[K] — that's an indexed access type, and it's why name is string and id is number rather than a vague string | number. The generic constraint K extends keyof T is one of the most common patterns in real TypeScript; see generics that don't make people cry for how constraints like this work.

typeof: turning a value into a type

In a type position, typeof captures the inferred type of a value. It's most powerful combined with as const, which freezes an object into deeply readonly literal types:

const config = {
  env: "production",
  retries: 3,
  featureFlags: ["search", "billing"],
} as const;
 
type Config = typeof config;
// {
//   readonly env: "production";
//   readonly retries: 3;
//   readonly featureFlags: readonly ["search", "billing"];
// }

Without as const, env would widen to string and retries to number. With it, you keep the exact literals — which is exactly what makes the derivations below precise. The as const guide covers that widening behavior in depth.

Indexed access types: T[K]

An indexed access type looks up the type of a property. It works with a single key, a union of keys, or the special number index for arrays:

type Env = Config["env"];          // "production"
type Flags = Config["featureFlags"]; // readonly ["search", "billing"]
 
// A union of keys yields a union of value types:
type Values = User[keyof User];    // number | string
 
// number indexes into an array/tuple element type:
const roles = ["admin", "editor", "viewer"] as const;
type Role = (typeof roles)[number]; // "admin" | "editor" | "viewer"

That last pattern — (typeof arr)[number] — is the idiomatic way to build a union type from a runtime array. Define the list once, derive the type from it, and the two can never disagree.

Step-by-step: a type-safe enum from an object

Here's the pattern that ties all three together. You want an object of constants and a type that is exactly its set of values — no duplication.

1. Define the object with as const:

const HttpStatus = {
  OK: 200,
  NotFound: 404,
  ServerError: 500,
} as const;

2. Derive the value type with typeof + indexed access:

type HttpStatusCode = (typeof HttpStatus)[keyof typeof HttpStatus];
// 200 | 404 | 500

3. Use it — and watch it stay in sync:

function handle(code: HttpStatusCode) {
  /* ... */
}
 
handle(HttpStatus.NotFound); // ok
handle(418);                 // error: 418 is not 200 | 404 | 500

Add Teapot: 418 to HttpStatus and HttpStatusCode automatically becomes 200 | 404 | 500 | 418. No second edit, no drift. This is the object-based alternative to enum that many teams prefer because it produces no extra runtime code and plays nicely with discriminated unions.

keyof vs typeof at a glance

They're easy to confuse because they often appear together. The distinction is what they consume and produce:

OperatorInputOutputExample
keyof Ta typeunion of its keyskeyof User"id" | "name"
typeof va valuethe type of that valuetypeof configConfig
T[K]type + keythe type of that propertyUser["id"]number

The mental model: typeof crosses from the value world into the type world; keyof and indexed access operate entirely inside the type world once you're there.

Best practices

  • Derive, don't duplicate. If a type mirrors a value's shape, compute it with typeof instead of writing it by hand.
  • Reach for as const first. Most typeof-based derivations only produce precise literal types when the value is as const.
  • Use (typeof arr)[number] for value unions. One array as the source of truth, a derived union that can't drift.
  • Constrain generics with K extends keyof T. It's the safe, reusable shape for any property-access helper.
  • Prefer indexed access over restating property types. T["field"] stays correct when T changes; a hand-copied type does not.

Common mistakes

  • Forgetting as const, then wondering why literals widened. typeof config gives you string/number without it — see the as const article.
  • Confusing runtime typeof with type-level typeof. typeof x === "string" is a runtime check; type T = typeof x is a compile-time type query. Same keyword, different worlds.
  • Using keyof on a type with an index signature. keyof { [k: string]: number } is string | number, not a nice literal union — that's expected, not a bug.
  • Over-deriving until nobody can read it. Deeply nested T[K1][K2][number] chains are powerful but can slow the compiler and hurt readability, as covered in why your types slow down the editor.

Takeaways

  • keyof turns a type into a union of its keys; typeof turns a value into its type; T[K] looks up a property's type.
  • Chained as (typeof obj)[keyof typeof obj], they derive a value-union type from any constant object.
  • Pair them with as const to preserve literal types and keep types in lockstep with data.
  • Deriving types eliminates the drift bugs you get from maintaining a type and a value by hand.

FAQ

What does keyof do in TypeScript? keyof takes a type and produces a union of its property keys as literal types. For an interface with keys id, name, and email, keyof yields "id" | "name" | "email", which you can then use to constrain generics or index into the type.

What is the difference between keyof and typeof? keyof operates on a type and returns the union of its keys. typeof (in a type position) operates on a value and returns that value's type. They're frequently combined as keyof typeof value to get the keys of an existing object.

What is an indexed access type? An indexed access type, written T[K], looks up the type of a property. User["id"] resolves to number. Using a union of keys, like User[keyof User], produces a union of all the property value types.

How do I get a union type from an array in TypeScript? Declare the array with as const and index it by number: (typeof roles)[number]. This derives a union of the array's element types that stays in sync with the array automatically.

Conclusion

keyof, typeof, and indexed access types are small operators with an outsized payoff: they let the compiler compute your types from the values you already have, so the two can never drift apart. Start by adding as const to your constant objects, derive value unions with (typeof x)[keyof typeof x], and constrain your helper generics with keyof. From there, the rest of the TypeScript cluster — from generics to the satisfies operator — builds on exactly these primitives.

References

8 min read

Read next