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.
# 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
| Form | What it produces |
|---|---|
typeof x (type position) | The static type of value x |
keyof T | Union 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 obj | Union 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'skeyof Array<T>, i.e."length" | "push" | "pop" | .... UseT[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
- Widening kills literal types — Without
as const,typeof { mode: "prod" }is{ mode: string }, not{ mode: "prod" }. Addas constwhenever you intendtypeofto capture narrow literals. keyof Array<T>looks tempting — it returns"length" | "push" | ... | number, not the element type. UseT[number]instead.keyof Ton a record with astringindex signature is juststring— Specific known keys are lost when there's a[k: string]: V. PreferRecord<SpecificKeys, V>when you want enumerable keys.keyof Ton a class skips private/protected members — Only public, non-static instance members appear. Usekeyof typeof Classfor the static side.- Symbols disappear if you cast to
string—keyof Tmay includesymbol. When you need string-only keys (for template literals, for example), intersect withstring:keyof T & string. typeofof aletwidens — Even with explicit literal types,let x: "a" | "b" = "a"; type X = typeof xis"a" | "b". To keep a single narrow type, useconst.- Optional properties surface as
T | undefinedin indexed access —User["email"]whereemail?: stringisstring | undefined. UseRequired<User>["email"]if you want to strip the optional. - Trying to use
typeofon a type name —typeof numberis an error.typeofonly works on a value binding. Use the type directly:type X = number. keyof typeof objincludes inherited keys whenobjis 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.- String-only iteration drops numeric keys — On a tuple,
[K in keyof T & string]skips0 | 1 | 2 | .... Usekeyof Twithout 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