Section 3 · Type-Level Programming
keyof, typeof and Indexed Access Types
16 min4 min readCode playground
Lesson at a glance
Derive types from runtime values and from other types with typeof, keyof and indexed access, so a change in one place flows through the codebase instead of drifting.
- 1typeof: from values to types
- 2keyof: the names of the properties
- 3Indexed access: the type of a property
- 4Why Object.keys returns string[]
No video here — just the real thing: a written lesson, runnable examples, a quiz and practice.
Cartwheel’s shipping zones live in a config object. The first version of the code described them twice: once as data, once as types.
const SHIPPING_ZONES = {
domestic: { countries: ['US', 'CA'], feeCents: 499 },
gulf: { countries: ['AE', 'SA'], feeCents: 1299 },
europe: { countries: ['GB', 'DE'], feeCents: 999 },
};
type Zone = 'domestic' | 'gulf' | 'europe'; // written by handSix months later someone added a levant zone to the object and forgot the type. Nothing failed to compile; the new zone just could not be selected anywhere. Every hand-written copy of a fact is a copy that can drift. This lesson covers the three operators that let you write the fact once and derive the rest.
typeof: from values to types
In a type position, typeof takes a value and gives you the type TypeScript inferred for it. It is not the JavaScript typeof operator (which returns strings like 'object' at runtime); it only exists at compile time.
const SHIPPING_ZONES = {
domestic: { countries: ['US', 'CA'], feeCents: 499 },
gulf: { countries: ['AE', 'SA'], feeCents: 1299 },
europe: { countries: ['GB', 'DE'], feeCents: 999 },
} as const;
type ShippingZones = typeof SHIPPING_ZONES;
// { readonly domestic: { readonly countries: readonly ['US', 'CA']; readonly feeCents: 499 }; … }as const matters here, as it did in Literal Types, as const and satisfies. Without it, countries would be string[] and feeCents would be number, and you would lose the details worth deriving.
typeof works on functions too: typeof formatPrice is the function’s full signature, which you can feed to utility types such as ReturnType<typeof formatPrice>. You will build ReturnType yourself in two lessons.
When should the value be the source and the type derived, rather than the other way round? Use typeof when the data is the definition: lookup tables, zone configs, lists of options that also render in the UI. Write the type first when the shape is a contract with someone else, such as an API payload, and check values against it.
keyof: the names of the properties
keyof T is the union of T’s property names:
type Zone = keyof typeof SHIPPING_ZONES; // 'domestic' | 'gulf' | 'europe'Add levant to the object and Zone updates itself. That is the whole point.
Two details are worth knowing. First, for a type with an index signature such as Record<string, number>, keyof gives string (and string | number for { [k: string]: … }, because JavaScript converts numeric keys to strings). Second, keyof on a union gives only the keys every member has. keyof (ShippedOrder | CancelledOrder) is 'id' | 'status', because reading trackingNumber from something that might be a cancelled order is not safe.
Indexed access: the type of a property
T[K] looks up the type of property K in T, using the same bracket syntax as runtime property access:
type GulfZone = ShippingZones['gulf']; // { readonly countries: …; readonly feeCents: 1299 }
type GulfFee = ShippingZones['gulf']['feeCents']; // 1299
type ZoneConfig = ShippingZones[Zone]; // union of all three configs
type FeeCents = ShippingZones[Zone]['feeCents']; // 499 | 1299 | 999
type ShippingCountry = ShippingZones[Zone]['countries'][number]; // 'US' | 'CA' | 'AE' | …Indexing with a union gives a union of the results, so ShippingZones[Zone] is “the value type of any zone”. The idiom (typeof X)[keyof typeof X] is how you get the union of an object’s values, and it comes up often enough that many codebases name it ValueOf<T>.
Arrays and tuples are indexed by number: T[number] is the element type. That is the (typeof COUNTRIES)[number] you used in section 1, and the last line above, which digs through two levels of objects and one array to collect every country in every zone.
The same trick works on API types you did not write. If a generated client gives you a big CheckoutResponse, you can name the line-item type as CheckoutResponse['lines'][number] instead of copying its fields.
Why Object.keys returns string[]
Sooner or later you will write this and be annoyed:
for (const zone of Object.keys(SHIPPING_ZONES)) {
SHIPPING_ZONES[zone]; // Error: 'string' can't be used to index type …
}Object.keys returns string[], not Zone[], and this is deliberate. Remember structural typing: a value whose type is { a: number } can be an object with ten more properties at runtime. If Object.keys promised (keyof T)[], it would be lying about those ten.
For a const object you defined yourself, with no way for extra keys to sneak in, a local cast is reasonable and honest: Object.keys(SHIPPING_ZONES) as Zone[]. For objects that came from elsewhere, keep string and check each key.
In the exercise you will derive five types from Cartwheel’s shipping config and API response. Next lesson goes one step further: instead of reading properties from a type, you will transform every property of a type at once.
Key takeaways
- In a type position,
typeof valuegives the type TypeScript inferred for that value, bridging runtime code into the type system. keyof Tis the union ofT’s property names, andT[K]is the type of the property (or properties) named byK.(typeof CONFIG)[keyof typeof CONFIG]gives the union of an object’s value types, andT[number]gives an array’s element type.Object.keysreturnsstring[]on purpose: structural typing means an object can have more keys than its type lists.