AcademyTypeScript
Deriving types with keyof and typeof
The three operators that read types out of things you already wrote, so a type never has to be maintained next to the value it describes.
3 min readUpdated 3 exercises
Stop writing types twice
Most type duplication in a codebase looks like this: an object, and a type describing that object, kept in step by hand. Three operators remove the second one.
| Operator | Reads | Gives |
|---|---|---|
keyof T | a type | the union of its keys |
typeof value | a value | its type |
T[K] | a type and a key | the type at that key |
keyof
interface User {
id: string;
email: string;
age: number;
}
type UserKey = keyof User; // 'id' | 'email' | 'age'On its own that is mildly useful. Combined with a generic, it is the single most common useful signature in TypeScript:
function get<T, K extends keyof T>(obj: T, key: K): T[K] {
return obj[key];
}
const user: User = { id: 'u1', email: 'a@b.c', age: 30 };
get(user, 'email'); // string
get(user, 'age'); // number
get(user, 'name'); // Error: 'name' is not assignable to 'id' | 'email' | 'age'The return type is not string | number — it is the type of the key you actually passed. That is T[K], indexed access, doing the work.
typeof crosses from values to types
There are two typeofs in TypeScript, and they live in different worlds. In an expression it is the JavaScript operator returning a string. In a type position it means "the type of that value":
const defaults = {
retries: 3,
timeout: 5_000,
verbose: false,
};
type Config = typeof defaults;
// { retries: number; timeout: number; verbose: boolean }
function configure(options: Partial<Config>) { /* ... */ }Add a field to defaults and Config grows with it. Nothing to keep in sync, because there is only one declaration.
Chain it with as const and indexed access and you get a union from a runtime array — the pattern that replaces most enums:
const ROLES = ['admin', 'editor', 'viewer'] as const;
type Role = (typeof ROLES)[number]; // 'admin' | 'editor' | 'viewer'[number] means "indexed by any number", which for a tuple is the union of all its elements.
Indexed access goes as deep as you like
interface ApiResponse {
data: {
users: { id: string; name: string }[];
};
}
type User = ApiResponse['data']['users'][number]; // { id: string; name: string }
type UserName = User['name']; // stringThis is how you name a type buried inside a response you did not declare — the generated client has one exported type, and you can reach every part of it without re-declaring any of it.
Where it adds up
Put together, these let you derive a whole family of types from one source of truth:
const HANDLERS = {
click: (x: number, y: number) => {},
keypress: (key: string) => {},
} as const;
type EventName = keyof typeof HANDLERS;
// 'click' | 'keypress'
type HandlerFor<E extends EventName> = (typeof HANDLERS)[E];
// HandlerFor<'keypress'> is (key: string) => voidkeyof typeof x reads right to left: take the type of the value, then its keys. It is the most common two-operator combination in the language, and once it is familiar you stop writing the union of names by hand — which means you stop forgetting to update it.
Now practice it
2 challenges · 1 quizReading this page is the cheap half. These are the exercises that make you use it.
- Utility Types — Record<K, V>ProRecord<K, V> is keyof and indexed access with a friendlier face, and the fastest way to get used to both.ChallengeSolve · EasyChallengeSolve · Easy
- Mapped Types — Nullable & OptionalProMapping over keyof T to make every field nullable, which is these operators doing something real.ChallengeSolve · MediumChallengeSolve · Medium
- Type System · Mapped & Conditional TypesMapped and conditional type questions that all start from keyof and an indexed access.QuizTake · MediumQuizTake · Medium
Read next
Generics: a type the caller fills in
What a type parameter actually does, why extends means constrain rather than inherit, and the rule that tells you when a generic is earning its keep.
3 min
Mapped and conditional types
How to write the utility types you have been importing: looping over keys, branching on a type, and pulling a type back out with infer.
4 min