skip to content

typeof & keyof — Type-level operators

TypeScript's typeof promotes a runtime value into the type position; keyof extracts the union of property keys. Together they form the backbone of type-safe lookups, enum-from-object patterns, and inferred shapes.

15 min read 36 snippets deep dive

typeof & keyof — Type-level operators#

What it is#

typeof and keyof are TypeScript’s two most-used type operators — small pieces of syntax that produce a new type from something else. typeof (in a type position) reads the type of a runtime value, letting you keep a single source of truth instead of duplicating the shape into a separate interface. keyof T produces the union of T’s public property keys, which is how every type-safe property accessor, mapped type, and string-literal lookup is built. The two pair up constantly: keyof typeof obj is the canonical way to derive an enum-like union from an as const object literal without writing an enum.

A note up front: the runtime typeof operator (typeof x === "string") is a completely different thing — same keyword, type-position vs value-position. The compiler decides which one you mean by where you wrote it.

Install#

No install. typeof and keyof are part of the TypeScript compiler — any tsc 2.1+ has them. The examples below assume TS 5.x.

# Verify your TypeScript version
npx tsc --version

Output:

Version 5.5.4

Syntax#

The shape of a type expression using these two operators:

typeof <value>           // promote a value's static type into a type
keyof <Type>             // union of <Type>'s public property keys
<Type>[<KeyExpr>]        // indexed access — look up a property type
typeof <value>[keyof typeof <value>]  // values-of pattern (very common)

Output: (none — type-only syntax, erased at compile time)

Essential forms#

FormWhat it produces
typeof x (type position)The static type of value x
keyof TUnion of T’s known keys (`string
T[K]The type of property K on T
T[keyof T]Union of all of T’s value types
keyof typeof objUnion of literal keys of a value obj
typeof obj[keyof typeof obj]Union of literal values of an as const object
T[number]Element type of an array/tuple
keyof T[] (= keyof Array<T>)Array’s own keys — almost never what you want

typeof in a type position#

In a value position, typeof x returns a string at runtime — "string", "number", "object", etc. In a type position (anywhere TypeScript expects a type), typeof x instead returns the static type that the compiler has inferred for the variable x. The two never overlap: the compiler knows which is which from context.

const config = {
  host: "localhost",
  port: 3000,
  tls: false,
};

// Type position — promote the value
type Config = typeof config;
// { host: string; port: number; tls: boolean }

// Value position — runtime check (different operator!)
if (typeof config.host === "string") {
  console.log(config.host.toUpperCase());
}

Output: (none — compile-time type alias; runtime console.log would print “LOCALHOST”)

The point: Config is now derived from config. Add a new property to config and Config updates automatically — no second declaration to keep in sync.

typeof on functions#

typeof works on any binding, including functions and classes. For a function, it gives you the full call signature; for a class, it gives you the constructor type (the thing new operates on).

function add(a: number, b: number): number {
  return a + b;
}

type AddFn = typeof add;
// (a: number, b: number) => number

// Useful with utility types
type AddReturn = ReturnType<typeof add>; // number
type AddParams = Parameters<typeof add>; // [a: number, b: number]

class Repo {
  constructor(public name: string) {}
}

type RepoCtor = typeof Repo;       // new (name: string) => Repo
type RepoInst = InstanceType<typeof Repo>; // Repo

Output: (none — all type-level)

typeof captures literal types only with as const#

By default, TypeScript widens literal values when assigning to a let or to a property of a mutable object. To preserve the narrow literal type, use as const:

const wide = { mode: "prod" };
type WideMode = typeof wide;
// { mode: string }   — widened to string

const narrow = { mode: "prod" } as const;
type NarrowMode = typeof narrow;
// { readonly mode: "prod" }

const arr = ["red", "green", "blue"] as const;
type Color = typeof arr[number];
// "red" | "green" | "blue"

Output: (none — pure type assertions)

The as const modifier marks the value as deeply readonly and keeps every literal narrow, which is what makes the rest of this article work.

keyof — the union of property keys#

keyof T produces a union of every public, declared key of T. For an interface User { id: number; name: string }, keyof User is "id" | "name". The operator works on type aliases, interfaces, classes, and on anything you can call typeof on.

interface User {
  id: number;
  name: string;
  active: boolean;
}

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

// Combined with a generic for type-safe property access
function get<T, K extends keyof T>(obj: T, key: K): T[K] {
  return obj[key];
}

const alice: User = { id: 1, name: "Alice", active: true };

const n = get(alice, "name");   // string
const a = get(alice, "active"); // boolean
// get(alice, "missing"); // Error — "missing" is not assignable to keyof User

Output: (none — type-only declarations; n === "Alice" at runtime)

keyof on a Record#

For Record<K, V>, keyof returns K (the keys you parameterised it with), not string. This is what makes Record enumerable at the type level.

type Role = "admin" | "user" | "guest";
type Perms = Record<Role, string[]>;

type RoleKey = keyof Perms;
// "admin" | "user" | "guest"

Output: (none)

keyof on a class#

keyof SomeClass gives you the instance members. To get the static side, take keyof typeof SomeClass.

class HttpClient {
  static defaultTimeout = 5000;
  baseUrl = "/";
  get(path: string): Promise<Response> {
    return fetch(this.baseUrl + path);
  }
}

type Instance = keyof HttpClient;
// "baseUrl" | "get"

type Static = keyof typeof HttpClient;
// "defaultTimeout" | "prototype"

Output: (none)

keyof any#

keyof any is string | number | symbol — the set of all possible JS object key types. It shows up in the definition of Record:

type Record<K extends keyof any, V> = { [P in K]: V };

Output: (none)

Indexed access — T[K]#

Indexed access reads the type of a property by key. It mirrors runtime obj[key] but operates on types.

interface User {
  id: number;
  name: string;
  address: { street: string; city: string };
}

type UserId      = User["id"];               // number
type Address     = User["address"];          // { street: string; city: string }
type AddressCity = User["address"]["city"];  // string

Output: (none)

Union keys give a union of value types#

When the key expression is itself a union, the result is a union of every matching property type.

type IdOrName = User["id" | "name"];
// number | string

// Get every value type
type AnyValue = User[keyof User];
// number | string | { street: string; city: string }

Output: (none)

Arrays and tuples — T[number]#

Array and tuple types are objects with numeric keys, so T[number] is the element type. This is the most common way to convert an as const tuple into a union.

const methods = ["GET", "POST", "PUT", "DELETE"] as const;
type Methods = typeof methods;        // readonly ["GET", "POST", "PUT", "DELETE"]
type Method  = typeof methods[number]; // "GET" | "POST" | "PUT" | "DELETE"

// Tuple — specific indices
type Tup = [string, number, boolean];
type First  = Tup[0];        // string
type Second = Tup[1];        // number
type Any    = Tup[number];   // string | number | boolean

Output: (none)

keyof T[] is not the array’s element type — it’s keyof Array<T>, i.e. "length" | "push" | "pop" | .... Use T[number] for elements.

keyof typeof — values to a key union#

Combining the two operators gives the most common real-world pattern: take an as const object literal, promote it to a type, then extract its keys as a union. This is how you build a type-safe enum without using enum.

const STATUS = {
  pending:  0,
  active:   1,
  inactive: 2,
  deleted:  3,
} as const;

type StatusKey   = keyof typeof STATUS;
// "pending" | "active" | "inactive" | "deleted"

type StatusValue = typeof STATUS[keyof typeof STATUS];
// 0 | 1 | 2 | 3

function setStatus(s: StatusKey): void {
  console.log("set status to", STATUS[s]);
}

setStatus("active");
// setStatus("paused"); // Error — not a key of STATUS

Output:

set status to 1

This is generally preferred over a runtime enum because it has zero emit (no helper object generated by the compiler), full string-literal narrowing, and survives JSON round-trips cleanly.

Combining with mapped types#

Mapped types use keyof as the iteration source. Pick, Omit, Partial, and friends are all [K in keyof T]: ... under the hood.

type Stringify<T> = {
  [K in keyof T]: string;
};

interface Form {
  age:    number;
  active: boolean;
  date:   Date;
}

type FormDraft = Stringify<Form>;
// { age: string; active: string; date: string }

// Filter by value type using `as` key remapping
type PickByValue<T, V> = {
  [K in keyof T as T[K] extends V ? K : never]: T[K];
};

type NumericFields = PickByValue<Form, number>;
// { age: number }

Output: (none)

typeof + keyof + generics — type-safe getters#

The classic property-access pattern combines all three: typeof to capture the source value, keyof to enumerate its keys, and a generic to thread the literal key through to the return type.

const palette = {
  primary:   "#8a5cff",
  secondary: "#ff7f50",
  text:      "#101015",
} as const;

function color<K extends keyof typeof palette>(key: K): typeof palette[K] {
  return palette[key];
}

const primary = color("primary"); // type: "#8a5cff"
console.log(primary);

Output:

#8a5cff

The return type is not string — it’s the narrow literal "#8a5cff". That comes from threading K all the way through and from palette being as const.

Distinguishing runtime typeof from type typeof#

These look identical but never collide:

const x: unknown = "hello";

// VALUE position — runtime check, returns a string
if (typeof x === "string") {
  console.log(x.toUpperCase());
}

// TYPE position — promote a binding's static type
const config = { port: 3000 };
type Config = typeof config;
//   ^^^^^^   ^^^^^^^^^^^^^^^^ — type expression; this `typeof` is the type operator

// You cannot apply runtime `typeof` to a *type*
// type Y = typeof number; // Error — 'number' refers only to a type

Output:

HELLO

Rule of thumb: if it sits to the right of : or inside a type/interface declaration, it’s the type operator. Anywhere else, it’s the runtime operator.

Common pitfalls#

  1. Widening kills literal types — Without as const, typeof { mode: "prod" } is { mode: string }, not { mode: "prod" }. Add as const whenever you intend typeof to capture narrow literals.
  2. keyof Array<T> looks tempting — it returns "length" | "push" | ... | number, not the element type. Use T[number] instead.
  3. keyof T on a record with a string index signature is just string — Specific known keys are lost when there’s a [k: string]: V. Prefer Record<SpecificKeys, V> when you want enumerable keys.
  4. keyof T on a class skips private/protected members — Only public, non-static instance members appear. Use keyof typeof Class for the static side.
  5. Symbols disappear if you cast to stringkeyof T may include symbol. When you need string-only keys (for template literals, for example), intersect with string: keyof T & string.
  6. typeof of a let widens — Even with explicit literal types, let x: "a" | "b" = "a"; type X = typeof x is "a" | "b". To keep a single narrow type, use const.
  7. Optional properties surface as T | undefined in indexed accessUser["email"] where email?: string is string | undefined. Use Required<User>["email"] if you want to strip the optional.
  8. Trying to use typeof on a type nametypeof number is an error. typeof only works on a value binding. Use the type directly: type X = number.
  9. keyof typeof obj includes inherited keys when obj is a class instance — For plain object literals it’s exactly the listed keys; for instances you get instance members regardless of where they were declared.
  10. String-only iteration drops numeric keys — On a tuple, [K in keyof T & string] skips 0 | 1 | 2 | .... Use keyof T without the intersection if numeric indices matter.

Real-world recipes#

Recipe 1 — Enum from an as const object#

You want enum-like values you can use both at runtime and in types, without enum’s peculiar emit. Build the object first, then derive the types from it.

const LOG_LEVEL = {
  trace: 10,
  debug: 20,
  info:  30,
  warn:  40,
  error: 50,
} as const;

type LogLevelName = keyof typeof LOG_LEVEL;
// "trace" | "debug" | "info" | "warn" | "error"

type LogLevelValue = typeof LOG_LEVEL[keyof typeof LOG_LEVEL];
// 10 | 20 | 30 | 40 | 50

function log(level: LogLevelName, msg: string): void {
  if (LOG_LEVEL[level] >= LOG_LEVEL.info) {
    console.log(`[${level}]`, msg);
  }
}

log("warn", "disk space low");
// log("verbose", "..."); // Error — not a LogLevelName

Output:

[warn] disk space low

Recipe 2 — Type-safe pluck over an array of objects#

pluck extracts a single field from each row of an array. With keyof and indexed access you get the right element type back, not any.

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

const users = [
  { id: 1, name: "Alice", age: 30 },
  { id: 2, name: "Bob",   age: 28 },
];

const names = pluck(users, "name"); // string[]
const ages  = pluck(users, "age");  // number[]

console.log(names.join(", "));

Output:

Alice, Bob

Recipe 3 — Configuration shape derived from defaults#

Build defaults as a plain object, then derive the both the resolved config type and the partial override shape from a single source. No interfaces to keep in sync.

const defaults = {
  host:    "localhost",
  port:    3000,
  retries: 3,
  ssl:     false,
} as const;

type Defaults  = typeof defaults;
type Resolved  = { -readonly [K in keyof Defaults]: Defaults[K] };
type Overrides = Partial<Resolved>;

function resolve(overrides: Overrides = {}): Resolved {
  return { ...defaults, ...overrides };
}

const cfg = resolve({ port: 8080 });
console.log(cfg);

Output:

{ host: 'localhost', port: 8080, retries: 3, ssl: false }

Recipe 4 — Typed event dispatcher from an event map#

Use an EventMap and keyof EventMap to make emit and on reject unknown event names while keeping payload types narrow.

type EventMap = {
  login:  { userId: string };
  logout: { reason: "manual" | "timeout" };
  view:   { route: string };
};

function makeBus<M extends Record<string, unknown>>() {
  type K = keyof M;
  const handlers = new Map<K, Array<(p: M[K]) => void>>();
  return {
    on<E extends K>(event: E, fn: (p: M[E]) => void): void {
      const list = (handlers.get(event) ?? []) as Array<(p: M[E]) => void>;
      list.push(fn);
      handlers.set(event, list as Array<(p: M[K]) => void>);
    },
    emit<E extends K>(event: E, payload: M[E]): void {
      const list = handlers.get(event) ?? [];
      for (const fn of list) (fn as (p: M[E]) => void)(payload);
    },
  };
}

const bus = makeBus<EventMap>();
bus.on("login", ({ userId }) => console.log("hello", userId));
bus.emit("login", { userId: "alice" });
// bus.emit("login", { userId: 123 }); // Error — userId must be string
// bus.emit("ping",  { foo: 1 });       // Error — not an event name

Output:

hello alice

Recipe 5 — Route table where paths are checked against a union#

You want a constant map of routes and you want the keys to be checked against a known set of path strings while keeping the literal types of each entry.

type RoutePath = `/${string}`;

const routes = {
  "/":            { layout: "home",   ssr: true  },
  "/about":       { layout: "static", ssr: true  },
  "/dashboard":   { layout: "app",    ssr: false },
} as const satisfies Record<RoutePath, { layout: string; ssr: boolean }>;

type Path   = keyof typeof routes;          // "/" | "/about" | "/dashboard"
type Layout = typeof routes[Path]["layout"]; // "home" | "static" | "app"

function go(path: Path): void {
  const r = routes[path];
  console.log(`-> ${r.layout} (ssr=${r.ssr})`);
}

go("/about");

Output:

-> static (ssr=true)

Recipe 6 — Narrow a value to a key with in keyof#

When you only know the key at runtime, use in keyof typeof obj style narrowing to convince TypeScript that key is safe to use as an index.

const palette = { primary: "#8a5cff", secondary: "#ff7f50" } as const;

function pick(key: string): string | undefined {
  if (key in palette) {
    // Inside this branch, `key as keyof typeof palette` is sound
    return palette[key as keyof typeof palette];
  }
  return undefined;
}

console.log(pick("primary"));
console.log(pick("missing"));

Output:

#8a5cff
undefined

Recipe 7 — Building a translator with dotted-path keys#

Use a recursive mapped type that walks keyof at every level to expose dotted paths through a nested object. Combined with typeof, the translator can never fall out of sync with the dictionary.

const dict = {
  user: {
    profile: { name: "Name", email: "Email" },
    actions: { logout: "Sign out" },
  },
  errors: { notFound: "Page not found" },
} as const;

type Dot<T, P extends string = ""> = {
  [K in keyof T & string]: T[K] extends Record<string, unknown>
    ? Dot<T[K], `${P}${K}.`>
    : `${P}${K}`;
}[keyof T & string];

type Key = Dot<typeof dict>;
// "user.profile.name" | "user.profile.email" | "user.actions.logout" | "errors.notFound"

function t(key: Key): string {
  return key.split(".").reduce<unknown>((acc, k) => (acc as Record<string, unknown>)[k], dict) as string;
}

console.log(t("user.profile.name"));
console.log(t("errors.notFound"));

Output:

Name
Page not found

Recipe 8 — Picking the keys whose values match a constraint#

Use mapped-key remapping to keep only the keys whose value type is assignable to a target. Powered entirely by keyof + indexed access.

interface Row {
  id:        number;
  name:      string;
  createdAt: Date;
  updatedAt: Date;
  active:    boolean;
}

type KeysOfType<T, V> = {
  [K in keyof T]: T[K] extends V ? K : never;
}[keyof T];

type DateKey   = KeysOfType<Row, Date>;   // "createdAt" | "updatedAt"
type StringKey = KeysOfType<Row, string>; // "name"

function toIso<K extends DateKey>(row: Row, key: K): string {
  return row[key].toISOString();
}

const row: Row = {
  id: 1, name: "Alice",
  createdAt: new Date("2026-01-01"),
  updatedAt: new Date("2026-01-02"),
  active: true,
};

console.log(toIso(row, "createdAt"));

Output:

2026-01-01T00:00:00.000Z