keyof, typeof and indexed access types

TypeScript · Advanced · 12 min

These three operators are the vocabulary for deriving types from other types and from real runtime values.

Three operators let you compute types instead of writing them out.

`typeof` in a type position gives the type of a variable or property: type Config = typeof defaultConfig. It's the bridge from values to types — write the object once and derive its type. It only accepts identifiers and property accesses, not arbitrary expressions (typeof f() is invalid; use ReturnType<typeof f>).

`keyof T` is the union of T's property names as literal types: keyof { a: 1; b: 2 } is "a" | "b". Two surprises:

  • For a type with a string index signature, keyof is string | number, because JavaScript converts numeric keys to strings.
  • keyof of a union only gives keys present in every member.

Indexed access types T[K] look up the type of a property: User["email"] is string. K can be a union (User["id" | "name"] gives the union of those property types) and must be a valid key, otherwise it's an error. Two common idioms:

  • T[keyof T] — the union of all property value types.
  • T[number] — the element type of an array or tuple type, e.g. (typeof roles)[number].

Combined they let a single runtime constant drive all its related types:

const roles = ["admin", "editor", "viewer"] as const;
type Role = (typeof roles)[number]; // "admin" | "editor" | "viewer"

Note the parentheses — typeof roles[number] parses the same way, but writing them makes intent clear. Indexed access works on types, not values: User["email"] is valid in a type, while const k = "email"; User[k] is not — use User[typeof k].

These operators are the inputs to mapped and conditional types, coming next.

Example

const defaultConfig = {
  host: "localhost",
  port: 8080,
  tls: { enabled: false, cert: "" },
};
type Config = typeof defaultConfig;          // derived from the value
type ConfigKey = keyof Config;               // "host" | "port" | "tls"
type Tls = Config["tls"];                    // { enabled: boolean; cert: string }
type ConfigValue = Config[keyof Config];     // string | number | { ... }

const roles = ["admin", "editor", "viewer"] as const;
type Role = (typeof roles)[number];          // "admin" | "editor" | "viewer"

function get<K extends ConfigKey>(key: K): Config[K] {
  return defaultConfig[key];
}

const port = get("port");                    // number
const tls: Tls = get("tls");
const role: Role = "editor";
type Dict = { [k: string]: boolean };
type DictKey = keyof Dict;                   // string | number
const v: ConfigValue = 1;
const dk: DictKey = 0;
console.log(port, tls.enabled, role, roles.includes(role), v, dk);

Remember

  • typeof value turns a runtime value into a type
  • keyof T = union of property names; T[K] = property type
  • T[number] gets an array/tuple's element type
  • Derive types from one constant instead of duplicating them

Watch out for

  • Writing typeof someFunction() — typeof only takes names; use ReturnType<typeof someFunction>.

Check yourself

  1. What is keyof { [key: string]: number }?
  2. Given const sizes = ["s", "m"] as const, which type is "s" | "m"?

Sign in to answer, see the explanations, and get this lesson back as a quick review later.

Official docs: www.typescriptlang.org/docs/handbook/2/indexed-access-types.html