codejump

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

Waiting at the end of this page.

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.

OperatorReadsGives
keyof Ta typethe union of its keys
typeof valuea valueits type
T[K]a type and a keythe type at that key

keyof

TypeScript
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:

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":

TypeScript
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:

TypeScript
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

TypeScript
interface ApiResponse {
  data: {
    users: { id: string; name: string }[];
  };
}

type User = ApiResponse['data']['users'][number];  // { id: string; name: string }
type UserName = User['name'];                      // string

This 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:

TypeScript
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) => void

keyof 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 quiz

Reading this page is the cheap half. These are the exercises that make you use it.