TypeScript 29 🔷 keyof, typeof, and Indexed Access Types
Three type operators sit at the heart of TypeScript’s type-level programming: keyof gives you the union of an object’s keys, typeof gives you the type of a value, and indexed access types (T[K]) give you the type of a property. Together they let you derive new types from existing ones — extracting keys, resolving values, and expressing relationships between types without duplicating them. They’re the foundation of mapped types, conditional types, and every utility type in the standard library. Once you understand them, a whole layer of TypeScript opens up.
Key point: keyof T produces a union of T‘s property names. typeof x produces the type of the value x. T[K] produces the type of the property K in T. They combine: typeof user gives the shape, keyof typeof user gives the keys, and (typeof user)[keyof typeof user] gives the value union. Every complex type-level operation starts with these three.
keyof — the union of keys
keyof T produces a union of all of T‘s property names.
interface User {
id: number;
name: string;
email: string;
}
type UserKeys = keyof User;
// 'id' | 'name' | 'email'
keyof User is a union of the three string literals. It’s a type — you can use it anywhere a type goes.
Using keyof in a type:
type UserKey = keyof User;
const key1: UserKey = 'id'; // ✅
const key2: UserKey = 'name'; // ✅
const key3: UserKey = 'missing'; // ❌
The union restricts what values are allowed.
Using keyof in a function:
function get<T, K extends keyof T>(obj: T, key: K): T[K] {
return obj[key];
}
const user: User = { id: 1, name: 'Alice', email: 'alice@example.com' };
get(user, 'name'); // string
get(user, 'id'); // number
get(user, 'missing'); // ❌
K extends keyof T requires key to be an actual key of obj. The return type T[K] is the type of that property.
keyof on an interface with an index signature:
interface Dict {
[key: string]: number;
}
type Keys = keyof Dict;
// string | number
An index signature adds string (or number) to the key union. That’s why Dict‘s keys are string | number — JavaScript objects can be indexed by both.
keyof on an array:
type ArrayKeys = keyof string[];
// number | 'length' | 'push' | 'pop' | 'map' | ...
An array’s keys include number (for indices) and all the method names.
keyof on a class:
class User {
id = 0;
name = '';
greet(): string { return `Hi, ${this.name}`; }
}
type UserKeys = keyof User;
// 'id' | 'name' | 'greet'
Class instances have both properties and methods. keyof includes both.
keyof on a type with no members:
type Empty = keyof {};
// never
An empty object has no keys, so the union is never.
Why keyof matters: It exposes the keys of a type as a union, which you can constrain type parameters against. Any function that takes a “property name” should use K extends keyof T. It’s the standard way to express “this parameter must be a key of that object.”
Why
keyofis idiomatic: It ties one type to another. A function that “gets a property by name” must know that name is a key of the object.keyofprovides the union; the constraint enforces membership. The compiler checks every call — typos fail, wrong types fail, and the return type is precise.
typeof — the type of a value
typeof x produces the type of the value x — at the type level.
const user = {
id: 1,
name: 'Alice',
email: 'alice@example.com'
};
type User = typeof user;
// { id: number; name: string; email: string }
typeof user extracts the type from the value user. You don’t have to write the shape twice — you derive it from the value.
typeof on primitives:
const name = 'Alice';
type Name = typeof name; // 'Alice' (const infers literal)
let count = 42;
type Count = typeof count; // number
typeof reflects the inferred type of the variable. const gives the literal; let gives the widened type.
typeof on functions:
function greet(name: string): string {
return `Hello, ${name}`;
}
type Greet = typeof greet;
// (name: string) => string
typeof greet is the function’s type — parameters and return type.
typeof on classes:
class User {
id = 0;
name = '';
}
type UserConstructor = typeof User;
// { new (): User; prototype: User }
type UserInstance = InstanceType<typeof User>;
// User
typeof User is the constructor type. InstanceType<typeof User> extracts the instance type.
typeof on modules and imports:
import * as fs from 'fs';
type FsType = typeof fs;
// The full type of the fs module
You can capture the type of an entire module.
typeof in a type context:
type User = typeof user;
// Combine with other operators
type UserKeys = keyof typeof user;
// 'id' | 'name' | 'email'
The typeof operator works in type positions — after keyof, inside generics, anywhere a type appears.
What typeof cannot do: It can’t produce a type from a value defined later in the file (hoisting is type-only, not value-order). It also can’t be used on expressions that aren’t simple identifiers or property accesses in some contexts.
Why
typeofmatters: It lets you derive types from values instead of declaring them twice. Define a constant object, and its type comes fromtypeof. Define a config, and its keys come fromkeyof typeof config. This is how you keep a single source of truth — the value — and let the type follow.
Indexed access types — T[K]
An indexed access type extracts the type of a property.
interface User {
id: number;
name: string;
email: string;
}
type UserId = User['id']; // number
type UserName = User['name']; // string
User['id'] is the type of the id property. It’s like accessing the property at runtime, but at the type level.
Multiple keys:
type UserContact = User['name' | 'email'];
// string | string → string
If the keys share a type, the union collapses. If they differ, the result is a union:
type UserValue = User['id' | 'name'];
// number | string
Combining with keyof:
type UserValue = User[keyof User];
// number | string
User[keyof User] gives the union of every property’s type. It’s a common pattern.
Indexed access with a variable key:
function get<T, K extends keyof T>(obj: T, key: K): T[K] {
return obj[key];
}
T[K] is the type of the property named by K. The constraint ensures K is a valid key.
Indexed access on arrays:
type StringArray = string[];
type Element = StringArray[number];
// string
type Tuple = [string, number];
type First = Tuple[0]; // string
type Second = Tuple[1]; // number
type All = Tuple[number]; // string | number
For arrays, [number] gives the element type. For tuples, [0] and [1] give specific positions, [number] gives the union of all elements.
Indexed access on objects with index signatures:
interface Dict {
[key: string]: number;
}
type Value = Dict[string]; // number
The index signature’s value type is accessible via [string].
Chained indexed access:
interface Company {
ceo: { name: string; age: number };
}
type CeoName = Company['ceo']['name'];
// string
You can chain — Company['ceo'] is an object, and ['name'] on it gives the name’s type.
Indexed access as a constraint:
function set<T, K extends keyof T>(
obj: T,
key: K,
value: T[K]
): void {
obj[key] = value;
}
value: T[K] means the value must match the property’s type. Setting user.name requires a string; setting user.id requires a number. The compiler catches mismatches.
Why indexed access matters: It’s how you express “the type of this property.” Without it, you’d have to redeclare the type. With it, you derive it. The combination with keyof — T[keyof T] — gives the union of all property types, which is used in mapped types, conditional types, and utilities like Partial<T>.
Why indexed access is powerful: It reads types the way code reads values.
user.namegets the value;User['name']gets the type. That symmetry is what makes TypeScript’s type-level programming feel like writing normal code — you’re accessing properties of types the same way you access properties of objects.
The three together
The operators combine naturally.
keyof typeof — keys of a value’s type:
const config = {
apiUrl: 'https://api.example.com',
timeout: 5000,
retries: 3
};
type ConfigKey = keyof typeof config;
// 'apiUrl' | 'timeout' | 'retries'
function getConfig(key: ConfigKey) {
return config[key];
}
getConfig('apiUrl'); // ✅
getConfig('missing'); // ❌
typeof config gives the shape; keyof gives the keys. Now you can iterate or pass valid keys.
typeof x[keyof typeof x] — value union:
type ConfigValue = typeof config[keyof typeof config];
// string | number
Every value in the config is either a string or a number. This pattern — typeof X[keyof typeof X] — is the standard way to extract the value union from a constant object.
From a tuple:
const ROLES = ['admin', 'user', 'guest'] as const;
type Role = typeof ROLES[number];
// 'admin' | 'user' | 'guest'
typeof ROLES is the tuple type; [number] extracts the element union.
From a config object:
const STATUS = {
Idle: 'idle',
Loading: 'loading',
Ready: 'ready'
} as const;
type StatusKey = keyof typeof STATUS;
// 'Idle' | 'Loading' | 'Ready'
type StatusValue = typeof STATUS[keyof typeof STATUS];
// 'idle' | 'loading' | 'ready'
This is the source-of-truth pattern: one runtime object, derived types for keys and values. Add a key, and both unions update.
From an enum-like object:
const HttpMethods = {
GET: 'GET',
POST: 'POST',
PUT: 'PUT',
DELETE: 'DELETE'
} as const;
type HttpMethod = typeof HttpMethods[keyof typeof HttpMethods];
// 'GET' | 'POST' | 'PUT' | 'DELETE'
Replaces an enum with a plain object and derived types — no runtime enum code, full type safety.
Why these combinations are everywhere: They let you derive types from values. Define a constant, and its keys and values become types. Add a key, and every union grows. This is the single-source-of-truth pattern applied to types — one place to change, and the type system follows.
Why
typeof X[keyof typeof X]is idiomatic: It’s the standard way to extract “all possible values” from a constant object. You’ll see it in every codebase that usesas constobjects for enums, config, or routes. It’s concise and precise — one expression that says “any value from this object.”
Practical patterns
The operators power a lot of real TypeScript.
Type-safe property getter:
function get<T, K extends keyof T>(obj: T, key: K): T[K] {
return obj[key];
}
const user = { id: 1, name: 'Alice' };
get(user, 'name'); // string
get(user, 'id'); // number
Type-safe property setter:
function set<T, K extends keyof T>(obj: T, key: K, value: T[K]): void {
obj[key] = value;
}
set(user, 'name', 'Bob'); // ✅
set(user, 'name', 42); // ❌ string expected
Pluck multiple properties:
function pluck<T, K extends keyof T>(items: T[], key: K): T[K][] {
return items.map(item => item[key]);
}
Pick properties:
function pick<T, K extends keyof T>(obj: T, keys: K[]): Pick<T, K> {
return keys.reduce((acc, k) => ({ ...acc, [k]: obj[k] }), {} as Pick<T, K>);
}
Iterate over keys:
function keys<T extends object>(obj: T): (keyof T)[] {
return Object.keys(obj) as (keyof T)[];
}
Group by a key:
function groupBy<T, K extends keyof T>(items: T[], key: K): Map<T[K], T[]> {
const map = new Map<T[K], T[]>();
for (const item of items) {
const k = item[key];
const list = map.get(k) ?? [];
list.push(k === undefined ? item : item);
map.set(k, list);
}
return map;
}
Sort by a key:
function sortBy<T, K extends keyof T>(items: T[], key: K): T[] {
return [...items].sort((a, b) => {
const av = a[key];
const bv = b[key];
return av < bv ? -1 : av > bv ? 1 : 0;
});
}
Each pattern uses keyof to constrain and T[K] to type the result.
Why these patterns are worth knowing: They cover most of what you’ll do with objects — read, write, iterate, sort, group, pick. Each is generic and type-safe. Once you know them, adapting to specific needs is quick, and the compiler catches mistakes.
A full example
A type-safe form handler using all three operators.
// ============================================
// FORM SHAPE
// ============================================
interface FormData {
name: string;
email: string;
age: number;
newsletter: boolean;
}
// ============================================
// TYPES DERIVED FROM THE SHAPE
// ============================================
type FormKey = keyof FormData;
// 'name' | 'email' | 'age' | 'newsletter'
type FormValue = FormData[FormKey];
// string | number | boolean
type FormErrors = Partial<Record<FormKey, string>>;
// { name?: string; email?: string; age?: string; newsletter?: string }
type FieldType<K extends FormKey> = FormData[K];
// ============================================
// FORM CLASS
// ============================================
class Form<K extends FormKey = FormKey> {
private values: Partial<FormData> = {};
private errors: FormErrors = {};
constructor(private fields: K[]) {}
set<K2 extends K>(field: K2, value: FormData[K2]): void {
this.values[field] = value;
delete this.errors[field];
}
get<K2 extends K>(field: K2): FormData[K2] | undefined {
return this.values[field] as FormData[K2] | undefined;
}
getValues(): Partial<FormData> {
return { ...this.values };
}
getErrors(): FormErrors {
return { ...this.errors };
}
setError<K2 extends K>(field: K2, message: string): void {
this.errors[field] = message;
}
isValid(): boolean {
return Object.keys(this.errors).length === 0;
}
// Iterate the fields with proper typing
forEach(callback: <K2 extends K>(field: K2, value: FormData[K2] | undefined) => void): void {
for (const field of this.fields) {
callback(field, this.values[field] as FormData[typeof field] | undefined);
}
}
}
// ============================================
// USAGE
// ============================================
const form = new Form(['name', 'email', 'age', 'newsletter']);
form.set('name', 'Alice'); // ✅
form.set('age', 30); // ✅
form.set('newsletter', true); // ✅
form.set('name', 42); // ❌ string expected
form.set('missing', 'x'); // ❌ not a field
form.setError('email', 'Required'); // ✅
form.setError('name', 'Too short'); // ✅
console.log(form.getValues());
console.log(form.getErrors());
console.log('Valid:', form.isValid());
// Typed getter
const name = form.get('name');
// string | undefined
const age = form.get('age');
// number | undefined
form.forEach((field, value) => {
console.log(`${field}: ${value}`);
});
What this shows:
FormKey— the union of form field namesFormValue— the union of all value typesFormErrors— a mapped type keyed by field namesFieldType<K>— the type of a specific fieldsetandget— constrained bykeyof FormDataand typed byFormData[K]
Every access is type-checked. Setting a string to age fails; reading name gives string | undefined.
Why this shape: It’s a realistic form handler where every operation preserves type information.
form.set('age', 30)is checked;form.get('name')returns the right type. The compiler enforces the relationship between field names and value types.
Complete Example Session
# ============================================
# PART 1: KEYOF
# ============================================
cat > keyof.ts << 'EOF'
interface User {
id: number;
name: string;
email: string;
}
type Keys = keyof User;
// 'id' | 'name' | 'email'
const k1: Keys = 'id'; // ✅
const k2: Keys = 'name'; // ✅
// const k3: Keys = 'x'; // ❌
console.log(k1, k2);
EOF
npx tsc --noEmit keyof.ts
# (no errors)
# ============================================
# PART 2: TYPEOF
# ============================================
cat > typeof.ts << 'EOF'
const user = {
id: 1,
name: 'Alice',
email: 'alice@example.com'
};
type User = typeof user;
// { id: number; name: string; email: string }
const u: User = { id: 2, name: 'Bob', email: 'bob@example.com' };
console.log(u);
EOF
npx tsc --noEmit typeof.ts
# (no errors)
# ============================================
# PART 3: INDEXED ACCESS
# ============================================
cat > indexed.ts << 'EOF'
interface User {
id: number;
name: string;
email: string;
}
type UserId = User['id']; // number
type UserName = User['name']; // string
type UserValue = User[keyof User]; // number | string
const id: UserId = 1;
const n: UserName = 'Alice';
const v: UserValue = 42;
console.log(id, n, v);
EOF
npx tsc --noEmit indexed.ts
# (no errors)
# ============================================
# PART 4: ARRAY AND TUPLE INDEXED ACCESS
# ============================================
cat > array.ts << 'EOF'
type Strings = string[];
type Element = Strings[number]; // string
type Tuple = [string, number, boolean];
type First = Tuple[0]; // string
type Second = Tuple[1]; // number
type All = Tuple[number]; // string | number | boolean
const e: Element = 'x';
const f: First = 'y';
const a: All = true;
console.log(e, f, a);
EOF
npx tsc --noEmit array.ts
# (no errors)
# ============================================
# PART 5: COMBINED PATTERNS
# ============================================
cat > combined.ts << 'EOF'
const config = {
apiUrl: 'https://api.example.com',
timeout: 5000,
retries: 3
} as const;
type ConfigKey = keyof typeof config;
// 'apiUrl' | 'timeout' | 'retries'
type ConfigValue = typeof config[keyof typeof config];
// 'https://api.example.com' | 5000 | 3
function getConfig(key: ConfigKey): ConfigValue {
return config[key];
}
console.log(getConfig('apiUrl'));
console.log(getConfig('timeout'));
// Add a const array
const ROLES = ['admin', 'user', 'guest'] as const;
type Role = typeof ROLES[number];
// 'admin' | 'user' | 'guest'
function setRole(r: Role): void { console.log(r); }
setRole('admin');
// setRole('owner'); // ❌
EOF
npx tsc --noEmit combined.ts
# (no errors)
# ============================================
# PART 6: GENERIC GET/SET
# ============================================
cat > getset.ts << 'EOF'
function get<T, K extends keyof T>(obj: T, key: K): T[K] {
return obj[key];
}
function set<T, K extends keyof T>(obj: T, key: K, value: T[K]): void {
obj[key] = value;
}
const user = { id: 1, name: 'Alice', active: true };
const n = get(user, 'name'); // string
const i = get(user, 'id'); // number
set(user, 'name', 'Bob');
set(user, 'active', false);
// set(user, 'name', 42); // ❌
console.log(n, i, user);
EOF
npx tsc --noEmit getset.ts
# (no errors)
# ============================================
# PART 7: COMPILE AND RUN
# ============================================
npx tsc keyof.ts typeof.ts indexed.ts array.ts combined.ts getset.ts
node keyof.js
# [ id name ]
node typeof.js
# [ { id: 2, name: 'Bob', email: 'bob@example.com' } ]
node indexed.js
# [ 1 Alice 42 ]
node array.js
# [ x y true ]
node combined.js
# [ https://api.example.com ]
# [ 5000 ]
# [ admin ]
node getset.js
# [ Alice 1 { id: 1, name: 'Bob', active: false } ]
Quick Reference
The Three Operators
| Operator | Meaning | Example |
|---|---|---|
keyof T | Union of T’s keys | keyof User → 'id' | 'name' |
typeof x | Type of value x | typeof user → shape |
T[K] | Type of property K | User['id'] → number |
keyof Results
| Type | keyof |
|---|---|
| Interface | Union of property names |
| Class | Union of properties + methods |
| Array | number | 'length' | 'push' | ... |
| Object with index sig | string | number |
| Empty object | never |
| Union of types | Union of keys (common ones) |
typeof Results
| Value | typeof |
|---|---|
const x = 5 | 5 |
let x = 5 | number |
const obj = { a: 1 } | { a: number } |
const arr = [1, 2] | number[] |
function f(): void {} | () => void |
class C {} | constructor type |
as const object | readonly literal shape |
Indexed Access Results
| Type | [K] |
|---|---|
User['id'] | number |
User[keyof User] | number | string |
string[] [number] | string |
[string, number][0] | string |
[string, number][number] | string | number |
Dict[string] | the value type |
Combined Patterns
| Pattern | Result |
|---|---|
keyof typeof x | Keys of x‘s type |
typeof x[keyof typeof x] | Union of all values |
typeof arr[number] | Element union from const array |
T[keyof T] | Value union of T |
Common Use Cases
| Use case | Pattern |
|---|---|
| Type-safe getter | <T, K extends keyof T>(o: T, k: K): T[K] |
| Type-safe setter | <T, K extends keyof T>(o: T, k: K, v: T[K]) |
| Pluck | <T, K extends keyof T>(xs: T[], k: K): T[K][] |
| Keys union | keyof typeof config |
| Values union | typeof config[keyof typeof config] |
| Enum from array | typeof ARR[number] |
Const Object Pattern
const STATUS = {
Idle: 'idle',
Ready: 'ready'
} as const;
type Key = keyof typeof STATUS;
// 'Idle' | 'Ready'
type Value = typeof STATUS[keyof typeof STATUS];
// 'idle' | 'ready'
Array/Tuple Access
| Type | Access | Result |
|---|---|---|
string[] | [number] | string |
readonly string[] | [number] | string |
[A, B] | [0] | A |
[A, B] | [1] | B |
[A, B] | [number] | A | B |
Errors and Fixes
| Error | Cause | Fix |
|---|---|---|
Property 'x' does not exist | Not a key | Use keyof T |
Cannot use typeof on ... | Not a value | typeof needs a value |
Type 'K' cannot be used to index | Not a key of T | Constrain to keyof T |
Object is possibly undefined | Optional property | Check or use ! |
Utility Types Built on These
| Type | Implementation |
|---|---|
Pick<T, K> | { [P in K]: T[P] } |
Omit<T, K> | Pick<T, Exclude<keyof T, K>> |
Partial<T> | { [P in keyof T]?: T[P] } |
Required<T> | { [P in keyof T]-?: T[P] } |
Record<K, V> | { [P in K]: V } |
Readonly<T> | { readonly [P in keyof T]: T[P] } |
Best Practices
✅ Do This:
// Constrain type parameters to keys
function get<T, K extends keyof T>(obj: T, key: K): T[K] {
return obj[key];
} // ✅
// Use typeof to derive types from values
const user = { id: 1, name: 'Alice' };
type User = typeof user; // ✅
// Use keyof typeof for keys of a value
const config = { apiUrl: '', timeout: 5000 };
type ConfigKey = keyof typeof config; // ✅
// Extract value unions from const objects
const ROLES = ['admin', 'user'] as const;
type Role = typeof ROLES[number]; // ✅
// Type the setter's value by the property
function set<T, K extends keyof T>(o: T, k: K, v: T[K]): void { } // ✅
// Use indexed access for property types
type UserId = User['id']; // ✅
// Chain indexed access
type City = Company['ceo']['address']['city']; // ✅
// Use `as const` with typeof for literal types
const STATUS = { Idle: 'idle' } as const;
type Status = typeof STATUS[keyof typeof STATUS]; // ✅
❌ Don’t Do This:
// Don't use string instead of keyof
function get<T>(obj: T, key: string) {
return obj[key]; // ❌ T has no index signature // ❌
}
// Don't duplicate types that typeof can derive
interface User { id: number; name: string; }
const u: User = { id: 1, name: 'Alice' };
// Instead of writing the interface, use typeof: // ✅
const u2 = { id: 1, name: 'Alice' };
type User2 = typeof u2;
// Don't forget as const for literal unions
const ROLES = ['admin', 'user']; // string[] // ⚠️
type Role = typeof ROLES[number]; // string // ⚠️
// Don't use T[K] without constraining K
function get<T, K>(obj: T, key: K): T[K] {
return obj[key]; // ❌ K not a key of T // ❌
}
// Don't assume typeof works on expressions
type X = typeof (1 + 2); // ⚠️ works but unusual // ⚠️
// Don't use indexed access for methods
type Bad = User['greet']; // ⚠️ may be a function type // ⚠️
// Don't skip the constraint in a generic setter
function set<T, K extends keyof T>(o: T, k: K, v: any): void { } // ⚠️
// Don't use keyof on primitives
type Bad = keyof number; // ❌ 'toString' | 'valueOf' | ... // ⚠️
Common Pitfalls
| Pitfall | Problem | Solution |
|---|---|---|
keyof on any | Returns string | number | symbol | Type properly |
Missing as const | Widened to string | Add as const |
Unconstrained K | Can’t index | K extends keyof T |
T[K] on non-key | Compile error | Constrain first |
typeof on type | Value only | Use T for types |
| Chained access | Need all keys | Make sure each link exists |
| Optional properties | T[K] includes undefined | Use -? modifier or handle |
| Index signature widening | Keys become string | Explicit type |
Real-World Examples
1. keyof on interface
interface User { id: number; name: string; }
type Keys = keyof User; // 'id' | 'name'
2. typeof on const
const user = { id: 1, name: 'Alice' };
type User = typeof user;
3. Indexed access
type UserId = User['id']; // number
4. Combined — keys of a value
const config = { api: '', timeout: 5000 };
type Key = keyof typeof config;
5. Combined — values of a value
type Value = typeof config[keyof typeof config];
6. Const array element union
const COLORS = ['red', 'green', 'blue'] as const;
type Color = typeof COLORS[number];
7. Const object value union
const STATUS = { Idle: 'idle', Ready: 'ready' } as const;
type Status = typeof STATUS[keyof typeof STATUS];
8. Type-safe getter
function get<T, K extends keyof T>(o: T, k: K): T[K] {
return o[k];
}
9. Type-safe setter
function set<T, K extends keyof T>(o: T, k: K, v: T[K]): void {
o[k] = v;
}
10. Pluck
function pluck<T, K extends keyof T>(xs: T[], k: K): T[K][] {
return xs.map(x => x[k]);
}
11. Pick
function pick<T, K extends keyof T>(o: T, ks: K[]): Pick<T, K> {
return ks.reduce((a, k) => ({ ...a, [k]: o[k] }), {} as Pick<T, K>);
}
12. Array element type
type Element = string[][number]; // string
13. Tuple element
type Pair = [string, number];
type First = Pair[0]; // string
type Second = Pair[1]; // number
14. Tuple union
type Both = Pair[number]; // string | number
15. Chained access
interface Company { ceo: { name: string } }
type CeoName = Company['ceo']['name']; // string
16. Function type from value
function greet(name: string): string { return name; }
type Greet = typeof greet; // (name: string) => string
17. Class constructor type
class User { }
type Ctor = typeof User; // constructor type
18. Instance type
class User { name = ''; }
type Instance = InstanceType<typeof User>; // User
19. Iterating keys
function keys<T extends object>(o: T): (keyof T)[] {
return Object.keys(o) as (keyof T)[];
}
20. Generic form field
function update<T, K extends keyof T>(obj: T, key: K, value: T[K]): T {
return { ...obj, [key]: value };
}
Visual: The Three Operators
┌──────────────────────────────────────────────┐
│ keyof │
│ │
│ keyof User │
│ │ │
│ ▼ │
│ 'id' | 'name' | 'email' │
│ │
│ → union of keys │
│ │
└──────────────────────────────────────────────┘
┌──────────────────────────────────────────────┐
│ typeof │
│ │
│ const user = { id: 1, name: 'Alice' } │
│ │ │
│ ▼ │
│ typeof user │
│ │ │
│ ▼ │
│ { id: number; name: string } │
│ │
│ → type of value │
│ │
└──────────────────────────────────────────────┘
┌──────────────────────────────────────────────┐
│ Indexed access T[K] │
│ │
│ User['id'] │
│ │ │
│ ▼ │
│ number │
│ │
│ → type of property │
│ │
└──────────────────────────────────────────────┘
Visual: Combined Usage
┌──────────────────────────────────────────────┐
│ const config = { │
│ apiUrl: 'https://x', │
│ timeout: 5000 │
│ } as const; │
│ │
└──────────────────────────────────────────────┘
│ │
│ keyof typeof │ typeof X[keyof typeof X]
▼ ▼
┌──────────────────────┐ ┌──────────────────────┐
│ 'apiUrl' | 'timeout'│ │ 'https://x' | 5000 │
│ │ │ │
│ keys of config │ │ values of config │
└──────────────────────┘ └──────────────────────┘
Visual: Type-Safe Getter
┌──────────────────────────────────────────────┐
│ function get<T, K extends keyof T>( │
│ obj: T, │
│ key: K │
│ ): T[K] { │
│ return obj[key]; │
│ } │
│ │
└──────────────────────────────────────────────┘
│
│ called with
▼
┌──────────────────────────────────────────────┐
│ get(user, 'name') │
│ │ │
│ ▼ │
│ K = 'name' │
│ T[K] = string │
│ │
│ get(user, 'id') │
│ K = 'id' │
│ T[K] = number │
│ │
│ get(user, 'x') │
│ ❌ not keyof User │
│ │
└──────────────────────────────────────────────┘
Visual: typeof on Values
┌──────────────────────────────────────────────┐
│ Value → Type │
│ │
│ const x = 5 │
│ type X = typeof x → 5 │
│ │
│ let y = 5 │
│ type Y = typeof y → number │
│ │
│ const obj = { a: 1 } │
│ type O = typeof obj → { a: number } │
│ │
│ const arr = [1, 2] as const │
│ type A = typeof arr → readonly [1, 2] │
│ │
└──────────────────────────────────────────────┘
Visual: Array and Tuple Access
┌──────────────────────────────────────────────┐
│ type Element = string[][number] │
│ // string │
│ │
└──────────────────────────────────────────────┘
┌──────────────────────────────────────────────┐
│ type Tuple = [string, number] │
│ │
│ Tuple[0] → string │
│ Tuple[1] → number │
│ Tuple[number] → string | number │
│ │
└──────────────────────────────────────────────┘
┌──────────────────────────────────────────────┐
│ const ROLES = ['admin', 'user'] as const; │
│ │
│ typeof ROLES → readonly [...] │
│ typeof ROLES[number] → 'admin' | 'user'│
│ typeof ROLES[0] → 'admin' │
│ │
└──────────────────────────────────────────────┘
Visual: Source of Truth Pattern
┌──────────────────────────────────────────────┐
│ Runtime value (source of truth) │
│ │
│ const STATUS = { │
│ Idle: 'idle', │
│ Ready: 'ready' │
│ } as const; │
│ │
└──────────────────────────────────────────────┘
│
│ derive types
▼
┌──────────────────────────────────────────────┐
│ Types │
│ │
│ type Key = keyof typeof STATUS │
│ // 'Idle' | 'Ready' │
│ │
│ type Value = typeof STATUS[keyof typeof STATUS]│
│ // 'idle' | 'ready' │
│ │
│ Add a key to STATUS → types update │
│ │
└──────────────────────────────────────────────┘
Visual: Utility Types Are Built on These
┌──────────────────────────────────────────────┐
│ Pick<T, K> │
│ = { [P in K]: T[P] } │
│ ↑ ↑ │
│ keyof indexed access │
│ │
│ Partial<T> │
│ = { [P in keyof T]?: T[P] } │
│ ↑ ↑ │
│ keyof indexed access │
│ │
│ Omit<T, K> │
│ = Pick<T, Exclude<keyof T, K>> │
│ ↑ │
│ keyof │
│ │
└──────────────────────────────────────────────┘
Visual: Chained Access
┌──────────────────────────────────────────────┐
│ interface Company { │
│ ceo: { │
│ name: string; │
│ address: { city: string }; │
│ }; │
│ } │
│ │
│ Company['ceo'] → { name, address }│
│ Company['ceo']['address'] → { city } │
│ Company['ceo']['address']['city'] → string │
│ │
└──────────────────────────────────────────────┘
Visual: Decision Flow
┌──────────────────────────────────────────────┐
│ Need the keys of a type? │
│ │ │
│ └── Yes ──► keyof T │
│ │
└──────────────────────────────────────────────┘
┌──────────────────────────────────────────────┐
│ Have a value but need its type? │
│ │ │
│ └── Yes ──► typeof value │
│ │
└──────────────────────────────────────────────┘
┌──────────────────────────────────────────────┐
│ Need a property's type? │
│ │ │
│ └── Yes ──► T[K] │
│ │
└──────────────────────────────────────────────┘
┌──────────────────────────────────────────────┐
│ Need the keys of a value's type? │
│ │ │
│ └── Yes ──► keyof typeof value │
│ │
└──────────────────────────────────────────────┘
┌──────────────────────────────────────────────┐
│ Need the values' union from a const object? │
│ │ │
│ └── Yes ──► typeof X[keyof typeof X] │
│ │
└──────────────────────────────────────────────┘
Summary
| Operator | Meaning | Example |
|---|---|---|
keyof T | Union of keys | keyof User → 'id' | 'name' |
typeof x | Type of value | typeof user → shape |
T[K] | Property type | User['id'] → number |
keyof typeof x | Keys of value’s type | 'apiUrl' | 'timeout' |
typeof x[keyof typeof x] | Value union | 'a' | 5 |
typeof arr[number] | Element union | 'admin' | 'user' |
Key takeaways:
keyof Tgives the union ofT‘s property namestypeof xgives the type of a value — a bridge from values to typesT[K]gives the type of propertyKinTkeyof+typeof—keyof typeof xgives the keys of a value’s typetypeof X[keyof typeof X]gives the union of all values in a const objecttypeof arr[number]gives the element union of aas constarray- Constrain type parameters with
K extends keyof Tfor type-safe property access T[K]makes setters type-safe —set(obj, 'name', 42)fails- Arrays and tuples support indexed access —
T[number],T[0] - Utility types like
Pick,Omit,Partial,Recordare built on these operators as constpreserves literal types — needed for value unions- Source-of-truth pattern — one runtime object, derived types for keys and values
Remember: keyof, typeof, and indexed access are the three primitives of TypeScript’s type-level programming. keyof exposes keys, typeof bridges values to types, and T[K] reads property types. Combine them — keyof typeof config, typeof STATUS[keyof typeof STATUS] — and you can derive any type from a value, keep a single source of truth, and write functions that are precise about what they accept. Every utility type and every mapped type is built on these three. Learning them opens the door to the rest of TypeScript’s type system.
Stop using slow, ad-bloated tool sites! 🤮
🔎 Search “KandZ Tools” on Google to use many professional utilities for free.
KandZ.me is the ultimate minimalist hub for:
✅ Finance (Mortgage, Interest, Inflation)
✅ Tech (Base64, JSON, Dev Suite, IP)
✅ Health (BMI, BMR, TDEE)
✅ Productivity (Timer, Workspace, QR)
⚡️ Fast & Private
🔒 No data leaves your device
💎 100% Free
🔗 Use it now: https://tools.kandz.me
🔖 Bookmark it—you’ll need it later!