Build a Type Safe Object Keys Helper

Build a generic helper that returns the keys of an object with precise types instead of string[]. Combine keyof, generics, and type assertions for safer iteration and lookup.

6 min read

The built-in Object.keys function returns string[] regardless of the object type. This means you lose the specific key names and cannot safely use them for property access. A TypeScript object keys helper fixes this by using generics to preserve the exact key types.

The problem is easy to see. Given a typed object, calling Object.keys gives you a generic string array:

typescripttypescript
const user = {
  id: 1,
  name: "Ada",
  email: "ada@example.com",
};
 
const keys = Object.keys(user);

The variable keys has type string[], not ("id" | "name" | "email")[]. If you try to use one of those keys to access a property on user, TypeScript complains because it cannot guarantee the string is a valid key. This is intentional: at runtime, objects can have extra properties that the type does not declare.

The solution is a small generic helper that narrows the return type:

typescripttypescript
function typedKeys<T extends Record<string, unknown>>(obj: T): (keyof T)[] {
  return Object.keys(obj) as (keyof T)[];
}

The function accepts any object-like type and returns an array of its keys. The as assertion tells TypeScript to trust that Object.keys returns the object's actual keys.

This is safe because Object.keys does return the runtime keys. The assertion only changes the compile-time type, not what actually happens when the code runs.

Using the Helper

With the helper in place, you can iterate over an object's keys the same way you always have, but every key now has its precise literal type instead of a generic string:

typescripttypescript
const userKeys = typedKeys(user);
 
for (const key of userKeys) {
  const value = user[key];
  console.log(`${key}: ${value}`);
}

TypeScript knows that each key is a valid property of user. The variable value has the correct type for each property, and you get full autocomplete on key inside the loop.

A Second Helper for Entries

A similar pattern gives you type-safe entries with both key and value correctly typed:

typescripttypescript
function typedEntries<T extends Record<string, unknown>>(
  obj: T
): [keyof T, T[keyof T]][] {
  return Object.entries(obj) as [keyof T, T[keyof T]][];
}

Each entry is a tuple of a key and its corresponding value type. You can destructure both in a loop:

typescripttypescript
for (const [key, value] of typedEntries(user)) {
  console.log(`${key} is ${typeof value}`);
}

The value has the union of all property types, which is correct because each iteration could yield any property. If you need more precise typing per iteration, you can narrow the key inside the loop using a type guard.

How the Generic Constraint Works

The constraint T extends Record<string, unknown> ensures the helper only accepts objects with string keys. Record is a built-in utility type. The unknown value type means the helper works with objects of any shape.

Inside the return type, keyof T produces the union of the object's property names. For the user object, that is "id" | "name" | "email". The array type (keyof T)[] wraps this union in an array.

The type assertion on the return value is necessary because TypeScript cannot verify at compile time that Object.keys returns exactly those keys. The runtime behavior is correct, so the assertion is the standard approach. For more on type assertions, see type assertions and type casting in TypeScript.

When Not to Use This Helper

The helper is most useful when you control the object type and know its shape is accurate. Skip it when:

  • The object comes from an external source and may have extra runtime properties not in the type.
  • You are dealing with index-signature types where keyof produces string | number.
  • You need to handle symbol keys, which keyof excludes by default.

In those cases, stick with Object.keys and narrow the keys manually where needed.

Why This Pattern Matters

Type-safe object keys are not just a convenience. They enable safer refactoring: if you rename a property, TypeScript flags every place the old key name is used. They also improve autocomplete in editors, so you see the actual property names when iterating instead of generic strings.

The helper is small enough to drop into a shared utilities file. It works with any object type without additional configuration. Once added, you get precise key types everywhere you iterate over objects in your codebase.

For more on the keyof operator that powers the keys helper, see keyof operator in TypeScript. For patterns that use these keys for type-safe property access, see indexed access types in TypeScript.

Rune AI

Rune AI

Key Insights

  • Object.keys returns string[] by design, losing the precise key types.
  • A generic helper with keyof and a type assertion restores the exact key union.
  • The helper gives you autocomplete on keys and type-safe property access.
  • Use a type assertion inside the helper, but keep the public API fully type-safe.
  • The helper works with any object type, including inferred types from object literals.
RunePowered by Rune AI

Frequently Asked Questions

Why does Object.keys return string[] instead of the actual keys?

TypeScript types are not exact at runtime. An object might have additional properties that the type does not declare, so Object.keys conservatively returns string[] to avoid giving a false sense of precision.

Is it safe to assert the return type with as?

The as assertion tells the compiler the result has a specific type, but it does not change the runtime value. It is safe here because Object.keys does return the actual keys at runtime. The assertion only narrows the TypeScript type.

Can this helper work with generic objects that have index signatures?

Yes, but the result will reflect the index signature. For an object typed as Record&lt;string, number&gt;, the keys helper returns string[], which is correct for index-signature types.

Conclusion

A type-safe object keys helper is a small utility with a big payoff. It replaces string[] with the actual key names, giving you autocomplete, type-safe iteration, and the ability to use keys in generic type operations. The implementation is simple: constrain the generic to an object shape, assert the return type, and let TypeScript verify the rest at compile time.