Skip to main content

TS Ep 27: The `keyof` Type Operator

Rachmat Hidayat
Author
Rachmat Hidayat
Learn & sharing insights on TypeScript, Go, Kubernetes, DevOps, DevSecOps, SRE, Platform Engineering, AI/ML Engineering, and MLOps.
typescript - This article is part of a series.
Part 27: This Article
The keyof operator takes an object type and produces a literal union of its keys. It acts as the type-level equivalent of JavaScript’s runtime Object.keys() function, enabling type-safe object property access and dynamic schema transformations.

1. What is keyof?
#

Given any object type T, keyof T queries the structural shape of T and returns a union of all allowed property key names:

interface Point3D {
  x: number;
  y: number;
  z: number;
}

// 🟢 Extract the keys of Point3D as a literal union
type Point3DKeys = keyof Point3D;
// Inferred Type: "x" | "y" | "z"

let validAxis: Point3DKeys = "x"; // Valid
validAxis = "z";                 // Valid

// ❌ Compiler Error: Type '"w"' is not assignable to type 'Point3DKeys'.
// validAxis = "w"; 

2. Type-Safe Property Accessors (getValue)
#

Without keyof, writing a generic property getter function in JavaScript requires resorting to any or loose string types, risking runtime undefined property bugs:

// ❌ UNSAFE: Allows passing non-existent string keys!
function getPropertyUnsafe(obj: any, key: string) {
  return obj[key];
}

By combining keyof with Generics (<T, K extends keyof T>), we force the caller to supply a key that definitely exists on the object, and we automatically infer the exact return type T[K]:

// 🟢 TYPE-SAFE: 'K' is constrained to valid keys of 'T', return type is T[K]
function getProperty<T, K extends keyof T>(obj: T, key: K): T[K] {
  return obj[key];
}

const user = {
  id: "usr_991",
  username: "rachmat",
  age: 30,
  isVerified: true,
};

// 1. 'key' parameter is restricted to "id" | "username" | "age" | "isVerified"
// 2. Return type is automatically inferred as 'string'!
const username = getProperty(user, "username"); 

// Return type is automatically inferred as 'number'!
const age = getProperty(user, "age"); 

// ❌ Compiler Error: Argument of type '"email"' is not assignable to parameter of type '"id" | "username" | "age" | "isVerified"'.
// getProperty(user, "email");

3. keyof on Index Signatures
#

How does keyof behave when applied to an object type that contains an Index Signature?

interface StringDictionary {
  [key: string]: boolean;
}

type DictKeys = keyof StringDictionary;
// Inferred Type: string | number

Why does keyof return string | number?
#

In JavaScript, object keys are automatically coerced to strings at runtime. If you execute dict[100], JavaScript coerces 100 to "100".

Therefore, TypeScript permits indexing a [key: string] dictionary with both string and number types!

interface NumberDictionary {
  [key: number]: string;
}

type NumDictKeys = keyof NumberDictionary;
// Inferred Type: number (ONLY number, because strings cannot be implicitly converted to numbers!)

4. keyof any (The Universal Key Constraint)
#

What happens if you run keyof any?

In JavaScript, valid object key types are strictly limited to string, number, and symbol. Therefore, keyof any evaluates to that exact primitive union:

type ValidKey = keyof any;
// Inferred Type: string | number | symbol

This is frequently used as a generic constraint when defining custom record types:

// Constrain K to valid object keys (string | number | symbol)
type CustomRecord<K extends keyof any, V> = {
  [P in K]: V;
};

5. Combining keyof with Indexed Access (T[keyof T])
#

By pairing keyof T with Indexed Access (T[...]), you can dynamically extract a union of all property value types inside an interface:

interface HTTPResponse {
  statusCode: number;
  statusText: string;
  isOk: boolean;
  headers: Record<string, string>;
}

// 🟢 Extract the union of ALL property value types:
type ResponseValueTypes = HTTPResponse[keyof HTTPResponse];
// Inferred Type: number | string | boolean | Record<string, string>

How this resolves:
#

  1. keyof HTTPResponse resolves to "statusCode" | "statusText" | "isOk" | "headers".
  2. HTTPResponse["statusCode" | "statusText" | "isOk" | "headers"] looks up all 4 properties simultaneously.
  3. Resolves to the union: number | string | boolean | Record<string, string>.

Summary & Next Steps
#

In this episode:

  • We queried object types with keyof T to extract literal key unions.
  • We built a type-safe generic property getter getProperty<T, K extends keyof T>(obj, key): T[K].
  • We analyzed keyof on index signatures ([key: string] $\to$ string | number).
  • We verified keyof any $\to$ string | number | symbol.
  • We combined keyof T with Indexed Access (T[keyof T]) to extract unions of property value types.

In Episode 28: The typeof Operator, we will explore how to query types directly from runtime JavaScript values!

typescript - This article is part of a series.
Part 27: This Article