keyof and typeof: Deriving Types From Values
AI generated
<T>
type
TypeScript · keyof · typeof · Type-Level Programming
keyof and typeof: Deriving Types From Values
Extracting object keys and value types with type safety

Maintaining TypeScript types manually alongside objects and constants eventually produces drift between code and type definition. The keyof and typeof operators derive types directly from existing values and interfaces, so object keys, configuration values and literal options stay automatically in sync while the compiler reliably rejects typos in property access.

12 min read keyof · typeof · type-level Generics · as const · indexed access types

1. Why deriving types from values beats duplicating them

In every larger TypeScript project, type definitions eventually appear that exactly mirror the structure of an already existing value: an interface that repeats the fields of a configuration object, or a union of string literals that exactly matches the allowed values of an array. This duplicate maintenance is one of the most common sources of silent inconsistency, because the compiler does not automatically keep two separate declarations in sync. If the object changes, the type quietly goes stale until an incorrect access slips through somewhere in the code.

The keyof and typeof operators solve this problem at the root by deriving types directly from existing values and type declarations instead of defining them manually. keyof extracts the keys of an object type as a union, typeof reads the static type of a concrete value. Together with as const and indexed access types, this forms a compact toolset for type-safe access functions, literal configuration values and utility types that automatically adapt to changes in the source code instead of requiring manual updates on every change.

2. keyof: the keys of an object type as a union

The keyof operator turns an object type into a union of string literal types that exactly match that type's keys. Given an interface User with the fields id, name, email and isActive, keyof User yields exactly the union 'id' | 'name' | 'email' | 'isActive'. This union can be used anywhere a loose string type was previously used, for example as the parameter type of a function that expects a valid property name. The decisive advantage over a manually written union: it automatically stays correct when a field is later added to or removed from the interface.

keyof works not only with interfaces but with any object type, including type aliases, class instance types and index signatures. With an index signature like [key: string]: number, keyof yields string | number rather than just string, because JavaScript internally treats numeric object keys as strings. Anyone who does not know this subtlety is often puzzled by a seemingly too-broad return type, even though the compiler is technically correct here.


// keyof extracts the union of an object type's keys as string literal types
interface User {
  id: number;
  name: string;
  email: string;
  isActive: boolean;
}

// UserKeys is the union: "id" | "name" | "email" | "isActive"
type UserKeys = keyof User;

function printKey(key: UserKeys): void {
  console.log(key);
}

printKey("name");     // OK
// printKey("age");   // Error: Argument of type '"age"' is not assignable...

// keyof also works with index signatures
interface Dictionary {
  [key: string]: number;
}

// DictKeys is "string | number" because JS object keys can be numeric strings
type DictKeys = keyof Dictionary; // string | number

3. keyof with generics: type-safe access to object properties

The real value of keyof shows up in combination with generics, when a function needs type-safe access to arbitrary properties of an arbitrary object type. The standard pattern is function getProp<T, K extends keyof T>(obj: T, key: K): T[K]. The constraint K extends keyof T ensures that only keys that actually exist on the passed object are accepted, and T[K] as the return type ensures the compiler infers the exact property type instead of a generic any or unknown.

Without this constraint, key would remain of type string, and the access obj[key] would fail to compile, because an arbitrary string is not a valid index for a concrete object type. This exact error shows how tightly keyof, generics and indexed access types work together: keyof supplies the allowed keys, the generic constraint binds them to the concrete object, and T[K] translates the key into the matching value type. The result is a reusable function that stays correctly typed for every object and every valid key, without any type assertions or any.


// Generic, type-safe property access: K is constrained to the keys of T
function getProp<T, K extends keyof T>(obj: T, key: K): T[K] {
  return obj[key];
}

interface Product {
  sku: string;
  price: number;
  inStock: boolean;
}

const product: Product = { sku: "MS-1001", price: 49.9, inStock: true };

const price = getProp(product, "price");   // inferred as number
const sku = getProp(product, "sku");       // inferred as string

// getProp(product, "weight"); // Error: "weight" is not assignable to keyof Product

// Without the constraint, key would be typed as `string` and the access
// would fail to compile, because an arbitrary string cannot index type T
function unsafeGetProp<T>(obj: T, key: string) {
  // return obj[key]; // Error: cannot use 'string' to index type 'T'
}

4. typeof: type-level operator vs. runtime operator

TypeScript overloads the keyword typeof with a second meaning that confuses many developers at first, because they already know the runtime operator from JavaScript. JavaScript's typeof evaluates at runtime and returns a string like 'number', 'string', 'object' or 'function', visible even in the compiled JavaScript output. This operator always sits in an expression position, for example in console.log(typeof value) or an if condition, and has existed unchanged since the early days of JavaScript.

TypeScript's typeof, by contrast, sits in a type position and is evaluated exclusively at compile time, leaving no trace whatsoever in the generated JavaScript. type Config = typeof config reads the type TypeScript inferred for the variable config and makes it reusable as a standalone type. The compiler distinguishes the two meanings purely by position: if typeof precedes an expression, it is JavaScript; if it follows type or appears in another type position, it is TypeScript's type-level operator.


// JavaScript's runtime typeof operator: evaluates at runtime, returns a string
const answer = 42;
console.log(typeof answer); // runtime typeof -> "number" (a string value)

// TypeScript's typeof operator: evaluates at compile time, returns a TYPE
const config = {
  apiUrl: "https://api.mironsoft.de",
  timeout: 5000,
  retries: 3,
};

// type-level typeof reads the inferred type of `config`, not a runtime string
type Config = typeof config;
// Config is: { apiUrl: string; timeout: number; retries: number }

// The two operators share a keyword but never overlap in position:
// - typeof x in an EXPRESSION position is JavaScript's runtime operator
// - typeof x in a TYPE position is TypeScript's compile-time operator
function describe(value: unknown): string {
  return typeof value; // runtime typeof, executes in the compiled JS output
}

let sameShape: typeof config; // type-level typeof, erased before runtime

5. typeof and keyof combined: deriving types from constants

The real strength of typeof shows up in combination with keyof, when types need to be derived not from a manually written interface but directly from an existing constant or configuration object. The pattern keyof typeof config first uses typeof to derive the full type of the object config, then uses keyof to extract the union of its keys. This eliminates any manual type declaration for objects whose structure already exists in the source code.

This pattern is especially useful for enum-like constant objects, for example a collection of feature flags or configuration keys that already exist in code as an object. Instead of manually maintaining a parallel interface or union type, the type is derived directly from the object and is guaranteed to stay in sync. If the object changes, say through a new feature flag, the derived type updates automatically on the next compilation, with nothing needing to be manually kept up to date anywhere in the code.

6. as const and typeof: literal union types from arrays and objects

Without additional hints, TypeScript widens the types of array and object literals to their general base types by default: an array of string literals becomes string[], a number becomes number. The as const assertion prevents this widening, makes the structure readonly, and preserves the exact literal type of each element. Combined with typeof and an indexed access type like (typeof roles)[number], this produces a precise union of exactly the literal values that actually occur in the array.

The same pattern also works for object literals: (typeof httpStatus)[keyof typeof httpStatus] yields the union of all values of an object declared with as const, for example 200 | 404 | 500 for a status code mapping. This approach replaces classic TypeScript enums in many cases, because it requires no additional runtime constructs and combines seamlessly with other utility types. Anyone who forgets as const loses the literal types and instead gets broad types like string or number, which renders the entire derived union type useless.


// Without "as const", TypeScript widens the array element type to string
const rolesWide = ["admin", "editor", "viewer"];
type RoleWide = typeof rolesWide[number]; // widened to: string

// "as const" makes the array readonly and preserves the literal types
const roles = ["admin", "editor", "viewer"] as const;

// typeof roles is: readonly ["admin", "editor", "viewer"]
// indexing with [number] yields the union of all literal element types
type Role = (typeof roles)[number]; // "admin" | "editor" | "viewer"

function assignRole(role: Role): void {
  // role is now restricted to the three literal strings above
}

assignRole("editor");   // OK
// assignRole("guest"); // Error: not assignable to type 'Role'

// The same pattern works on object literals for their value union
const httpStatus = {
  OK: 200,
  NOT_FOUND: 404,
  SERVER_ERROR: 500,
} as const;

type HttpStatusCode = (typeof httpStatus)[keyof typeof httpStatus]; // 200 | 404 | 500

7. Indexed access types: using T[K] deliberately

Indexed access types use the same square-bracket syntax as a runtime property access to read the type of a single property. Order['total'] yields the type of the total property, and these accesses can be nested arbitrarily: Order['customer']['name'] navigates through nested object structures exactly the way the corresponding runtime access would. The key inside the square brackets must be an actual key of the type, otherwise the compiler reports an error.

If a union is passed instead of a single key, for example Order['id' | 'total'], TypeScript returns the union of the corresponding property types, in this case string | number. Combining this directly with keyof, T[keyof T] yields the union of all of an object's property types. This pattern forms the foundation of many built-in utility types like Pick and Record, and can be reused in custom generic helper types too, for example to extract the type of an arbitrary but guaranteed-valid property.


interface Order {
  id: string;
  total: number;
  customer: {
    name: string;
    email: string;
  };
}

// Indexed access with a single literal key
type OrderId = Order["id"]; // string

// Nested indexed access reaches into a property's own properties
type CustomerName = Order["customer"]["name"]; // string

// Indexed access with a union of keys returns a union of the matching types
type OrderPrimitive = Order["id" | "total"]; // string | number

// Combined with keyof, T[keyof T] yields the union of ALL property types
type OrderValue = Order[keyof Order]; // string | number | { name: string; email: string }

// A generic helper that extracts a nested property type safely
type PropType<T, K extends keyof T> = T[K];
type TotalType = PropType<Order, "total">; // number

8. Common pitfalls with keyof and typeof

The most common confusion arises when developers try to determine the type of a value using the wrong typeof: JavaScript's typeof in a condition returns a string at runtime and is well suited for type guards, but not for producing a reusable TypeScript type. Anyone who writes type X = typeof someValue assuming it builds a runtime check is conflating the two meanings and gets sent in a different direction than intended by the compiler.

A second pitfall involves forgetting as const: without the assertion, TypeScript widens array literals, so (typeof arr)[number] becomes just string or number instead of the expected literal union. A third pitfall is keyof on classes: only public members appear in the union, private and protected properties are excluded, which leads to unexpectedly smaller types during refactoring. Anyone who knows these three patterns, JS typeof versus TS typeof, as const versus widening, and visibility on classes, avoids the vast majority of confusion around both operators.

9. keyof and typeof compared

The following overview contrasts imprecise, manually maintained patterns with the corresponding idiomatic solutions using keyof and typeof. In every row, the left column leads to drift between code and type or to weaker type safety, while the right column derives the type directly from the actual value.

Scenario Imprecise / error-prone Idiomatic with keyof/typeof Benefit
Union for object keys 'id' | 'name' | 'email' written by hand keyof User Stays automatically in sync with the interface
Property access function getProp(obj: any, key: string): any getProp<T, K extends keyof T>(obj: T, key: K): T[K] Return type correctly inferred, invalid keys rejected
Type derivation from a value typeof value === 'object' misused as a type alias type X = typeof constValue Avoids confusing JS and TS typeof
Literal options from an array const roles: string[] = [...] as const + typeof roles[number] Only valid literals allowed, no arbitrary strings
Type for a config object Interface maintained in parallel to the object type Config = typeof configObject Single source of truth, no drift

In practice, the right column is almost always worth it, even when the manual variant looks simpler at first glance: the effort for keyof and typeof is spent once at definition time, while the manual alternative needs to be maintained again with every future change. This investment pays off especially quickly in codebases with frequently changing configuration objects or growing interfaces.

Mironsoft

TypeScript tooling, type safety and build scripts for Magento and Hyva projects

Looking for real TypeScript type safety on your project?

We bring genuine type safety to your TypeScript codebase, from generic utility types through keyof/typeof patterns to cleanly typed build and integration scripts for Magento and Hyva projects.

Type Safety Audit

Analysis of existing type definitions for drift, any leaks, and missing generic constraints

Utility Types & Refactoring

Using keyof, typeof, as const and mapped types deliberately to eliminate duplication

Build & Tooling Scripts

Type-safe scripts for deployment, code generation and headless integrations

10. Summary

keyof and typeof together solve a recurring problem in typed codebases: types maintained manually alongside an already existing value eventually drift apart. keyof extracts the keys of an object type as a union and makes them usable in generic functions like getProp<T, K extends keyof T>. typeof reads the type of a concrete value at compile time and is fundamentally different from the runtime operator of the same name in JavaScript, even though both share the same keyword.

Combined with as const and indexed access types like T[K], this produces a consistent pattern for creating literal union types from arrays and objects, type-safe property access, and configuration types kept in sync, all without duplicate type declarations. Anyone who applies these operators consistently not only reduces the maintenance burden of type definitions but also lets the compiler reliably catch real typos in property names and configuration values before they ever reach production.

keyof and typeof: The Essentials at a Glance

keyof basics

Extracts the keys of an object type as a union of string literal types. Stays automatically in sync with the source type.

typeof in the type context

Reads the static type of a value at compile time. Completely different from the JavaScript runtime operator.

as const + typeof

Prevents literal type widening and, combined with indexed access types, produces precise literal unions.

Generics with K extends keyof T

Makes property access functions type-safe: invalid keys are rejected, return types correctly inferred.

11. FAQ: keyof and typeof in TypeScript

1What does the keyof operator do in TypeScript?
Produces a union of all keys of an object type as string literal types. Stays automatically in sync as soon as the object type changes.
2Difference between TS typeof and JS typeof?
JS typeof is a runtime operator that returns a string. TS typeof sits in a type position, is evaluated at compile time, and returns a static type.
3How do I combine keyof and typeof?
keyof typeof config first derives the object's type with typeof, then extracts the union of its keys with keyof. Avoids duplicate type declarations.
4What does as const do with typeof?
Prevents literal type widening and makes the structure readonly. Combined with typeof, it produces a precise literal union instead of string or number.
5How does getProp work with K extends keyof T?
The constraint only accepts keys that actually exist on T. T[K] as the return type automatically infers the correct property type.
6What are indexed access types (T[K])?
Use the same syntax as a runtime property access to read the type of a property, for example Order['total']. With a union of keys, they produce a union of types.
7Can I apply keyof to a class?
Yes, works on the instance type just like on an interface. Private and protected properties are not included.
8Why does keyof return string | number for index signatures?
Because JavaScript treats numeric object keys internally as strings. TypeScript reflects this by allowing both types in the union.
9Difference between keyof T and (keyof T)[]?
keyof T is a union type of individual keys. (keyof T)[] is an array type whose elements are each of type keyof T.
10Does typeof work with imported values?
Yes, typeof works regardless of whether a value was declared locally or imported from another module.