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