TypeScript Advanced #1: keyof and typeof
If you’ve finished TypeScript Basics and + React in Practice, it’s time to take it up a level — the tools for shaping types directly. This series is seven posts.
- #1 keyof and typeof ← this post
- #2 Mapped types
- #3 Conditional types and infer
- #4 Template literal types
- #5 Discriminated unions and type guards in depth
- #6 Modules and .d.ts
- #7 Practical patterns and anti-patterns
If the basics course was about “how to write types,” this series is about “how to compute types.” The first tools are the most fundamental two — keyof and typeof.
keyof — gathering keys of an object type into a union
#
keyof T turns every key of an object type T into a union of string literals.
type User = {
id: string;
name: string;
age: number;
};
type UserKey = keyof User;
// 'id' | 'name' | 'age'
It seems unremarkable at first, but it’s what makes “one of T’s keys” expressible. Its most common use is functions that access object properties safely.
function getValue<T, K extends keyof T>(obj: T, key: K): T[K] {
return obj[key];
}
const user: User = { id: 'u1', name: '커티스', age: 30 };
getValue(user, 'name'); // string
getValue(user, 'age'); // number
getValue(user, 'unknown'); // ✗ 'unknown' is not keyof User
A three-line function that guarantees two things at once.
- Nonexistent keys are blocked at the call site
- The return type narrows by key —
namegivesstring,agegivesnumber
T[K] is an indexed access type. It means “the type of the value pulled out of T at key K.” It’s intuitive because it mirrors JavaScript’s obj[key] syntax.
Index signature vs explicit keys #
The result of keyof depends on how the object is defined.
type A = { id: string; name: string };
type AK = keyof A; // 'id' | 'name' (narrow union)
type B = { [key: string]: string };
type BK = keyof B; // string | number (number is included because JS auto-converts numeric keys)
Only explicitly written keys are captured as narrow literals. Using an index signature reduces the precision of keyof. From a type-safety standpoint, it’s better to write object shapes explicitly when possible.
Number keys appear in the union too — keyof on arrays #
keyof on an array type includes the indices (numbers) and method names.
type Arr = string[];
type ArrK = keyof Arr;
// number | 'length' | 'toString' | 'push' | ...
You almost never use keyof on arrays directly. The pattern of pulling out the element type with T[number] is far more common.
type Arr = { id: string; name: string }[];
type Item = Arr[number]; // { id: string; name: string }
number here means “the type when accessed at any numeric index.” It’s the shortest pattern for element-type extraction in non-tuple arrays.
typeof — pulling a type out of a value
#
TypeScript’s typeof shares the name with JavaScript’s typeof but appears in a different position.
- JavaScript: in a value expression —
typeof x === 'string'(value level) - TypeScript: in a type expression —
let y: typeof x(type level)
const config = {
baseUrl: 'https://api.example.com',
timeout: 5000,
};
type Config = typeof config;
// { baseUrl: string; timeout: number }
Pull the type from a value you’ve already written. The core benefit is writing the value once and having the type follow automatically. It eliminates the duplication of writing the same information in two places.
Functions, imports too #
typeof works on any value.
function createUser(name: string, age: number) {
return { id: crypto.randomUUID(), name, age };
}
type CreateUser = typeof createUser;
// (name: string, age: number) => { id: string; name: string; age: number }
type User = ReturnType<typeof createUser>;
// { id: string; name: string; age: number }
The ReturnType<typeof fn> pattern is used very often. It pulls “the type of the object this function returns” without writing it separately. We’ll build ReturnType itself in #3.
import * as utils from './utils';
type Utils = typeof utils; // type of the entire module
keyof typeof — the most common combination
#
keyof takes a type, and typeof makes a type from a value. Combine them and you can pull the keys of an object defined as a value into a union.
const STATUS = {
idle: 'idle',
loading: 'loading',
done: 'done',
error: 'error',
} as const;
type StatusKey = keyof typeof STATUS;
// 'idle' | 'loading' | 'done' | 'error'
type StatusValue = typeof STATUS[StatusKey];
// 'idle' | 'loading' | 'done' | 'error'
The core idea: define an enum-like object in JavaScript and automatically derive a union type from it. Add a key to the object and the union grows automatically. It’s a very valuable pattern for creating a single source of truth.
as const is the key
#
Without as const, the same code falls apart.
const STATUS = {
idle: 'idle',
loading: 'loading',
};
type StatusValue = typeof STATUS[keyof typeof STATUS];
// string ← too wide
as const is what makes TypeScript narrow each value to a literal type. It’s the signal “fix this object or array exactly as this shape and exactly as these values.” Without as const, everything widens to plain string or number.
The effect of as const shown in a table:
const a = ['red', 'green', 'blue'];
// ^ string[]
const b = ['red', 'green', 'blue'] as const;
// ^ readonly ['red', 'green', 'blue']
const c = { mode: 'dark' };
// ^ { mode: string }
const d = { mode: 'dark' } as const;
// ^ { readonly mode: 'dark' }
Adding as const makes the type exactly the value. Arrays become readonly tuples; objects become literal types where every field is readonly. Remember it as “freeze it as I wrote it” and it’s intuitive.
Real-world 1 — building a union from a route map #
When you define API routes or page routes as an object and want to use the keys as a safe union.
const ROUTES = {
home: '/',
about: '/about',
blog: '/blog',
blogPost: '/blog/:slug',
} as const;
type RouteName = keyof typeof ROUTES;
// 'home' | 'about' | 'blog' | 'blogPost'
type RoutePath = typeof ROUTES[RouteName];
// '/' | '/about' | '/blog' | '/blog/:slug'
function navigate(name: RouteName) {
const path = ROUTES[name];
// ...
}
navigate('home'); // OK
navigate('contact'); // ✗ contact is not defined
Add a new route to ROUTES and RouteName extends automatically. Define once → types sync automatically.
Real-world 2 — auto-collecting action types #
A pattern for defining reducer actions as an object and auto-generating the union from it.
const ActionTypes = {
ADD_TODO: 'todos/add',
REMOVE_TODO: 'todos/remove',
TOGGLE_TODO: 'todos/toggle',
} as const;
type ActionType = typeof ActionTypes[keyof typeof ActionTypes];
// 'todos/add' | 'todos/remove' | 'todos/toggle'
With this in place, calls like dispatch({ type: ActionTypes.ADD_TODO, ... }) line up exactly with the types and you don’t need to maintain the type definitions separately.
Where keyof shines — form field validation
#
When you handle a form as an object and want to narrow the field name as a type.
type SignupForm = {
email: string;
password: string;
agree: boolean;
};
function setField<K extends keyof SignupForm>(
form: SignupForm,
key: K,
value: SignupForm[K]
): SignupForm {
return { ...form, [key]: value };
}
const form: SignupForm = { email: '', password: '', agree: false };
setField(form, 'email', 'me@example.com'); // OK
setField(form, 'agree', true); // OK
setField(form, 'agree', 'yes'); // ✗ agree is boolean
setField(form, 'unknown', '...'); // ✗ unknown key
In a three-line function, you get blocking nonexistent keys and per-key value-type matching at the same time. With JavaScript you’d have to enforce these in docs or with if-statements.
Wrap-up #
What this post covered:
keyof T— union of an object type’s keysT[K]— indexed access type, pulling a value type by keyT[number]— extracting an array’s element typetypeof value— pulling a type from a value, type-level expressionas const— fixing literal types as-is (objects + arrays)keyof typeof OBJ— auto-generating a key union from an object defined as a value- Real-world — route maps, action types, safe form-field validation
Once these two tools are second nature, the foundation for the next tools is in place. In the next post (#2 Mapped types) we go one step further on top of keyof — mapped types that transform an entire object type. We’ll also build built-in utilities like Partial, Required, and Readonly ourselves to see how they’re constructed.