cheat sheet

typeof & keyof

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.

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.

bash
# Verify your TypeScript version
npx tsc --version

Output:

code
Version 5.5.4

Syntax

The shape of a type expression using these two operators:

typescript
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.

typescript
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).

typescript
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:

typescript
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.

typescript
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.

typescript
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.

typescript
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:

typescript
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.

typescript
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.

typescript
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.

typescript
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.

typescript
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:

vbnet
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.

typescript
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.

typescript
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:

bash
#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:

typescript
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:

code
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.

typescript
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:

csharp
[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.

typescript
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:

code
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.

typescript
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:

yaml
{ 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.

typescript
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:

code
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.

typescript
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:

ini
-> 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.

typescript
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:

bash
#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.

typescript
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:

css
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.

typescript
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:

makefile
2026-01-01T00:00:00.000Z