Skip to content

Improve control-flow narrowing for Object.hasOwn(obj, key)Β #63490

@eng-same

Description

@eng-same

πŸ” Search Terms

Object.hasOwn narrowing
Object.hasOwn key narrowing
hasOwn keyof
hasOwn property type guard
hasOwn own property narrowing
Object.hasOwn Record
Object.hasOwn unknown object
hasOwnProperty narrowing

βœ… Viability Checklist

⭐ Suggestion

I would like TypeScript to improve control-flow narrowing when using Object.hasOwn(obj, key).

Object.hasOwn() is the modern standard JavaScript API for checking whether an object has an own property. However, after checking Object.hasOwn(obj, key), TypeScript does not always narrow the relationship between obj and key in a way that makes safe property access ergonomic.

The suggestion is for TypeScript to treat successful Object.hasOwn(obj, key) checks similarly to a built-in type guard, where possible.

For example, inside this block:

if (Object.hasOwn(obj, key)) {
  // TypeScript should know that `key` is an own key of `obj`
}

TypeScript could narrow key relative to obj, or narrow obj to include the checked property when the key is a string/number/symbol literal or a narrowed union.

πŸ“ƒ Motivating Example

When working with data from APIs, dashboards, configuration objects, or dictionaries, developers often need to check whether a dynamic key exists before accessing it.

Today, the runtime check is clean:

const labels = {
  pending: "Pending",
  approved: "Approved",
  rejected: "Rejected",
} as const;

function getLabel(status: string) {
  if (Object.hasOwn(labels, status)) {
    return labels[status];
    // Currently this can still produce an error because `status`
    // is still just `string`, not narrowed to keyof typeof labels.
  }

  return "Unknown";
}

Developers know this is safe at runtime, because Object.hasOwn(labels, status) verifies that the property exists. But TypeScript does not fully use that information for narrowing.

A more ergonomic behavior would be:

function getLabel(status: string) {
  if (Object.hasOwn(labels, status)) {
    // status is narrowed to keyof typeof labels
    return labels[status];
  }

  return "Unknown";
}

### πŸ’» Use Cases

### 1. What do you want to use this for?

I want to use `Object.hasOwn()` as the standard way to safely narrow dynamic object keys before property access.

Common examples include:

- mapping backend enum/status values to UI labels
- reading configuration objects
- validating API response fields
- checking feature flags
- accessing dictionary-style objects
- working with `Record<string, unknown>` or partially known objects

Example:

```ts
const statusLabels = {
  active: "Active",
  blocked: "Blocked",
  pending: "Pending",
} as const;

function formatStatus(status: string) {
  if (Object.hasOwn(statusLabels, status)) {
    return statusLabels[status];
  }

  return "Unknown";
}

2. What shortcomings exist with current approaches?

Currently, developers often need to use extra assertions or custom helper functions:

if (Object.hasOwn(statusLabels, status)) {
  return statusLabels[status as keyof typeof statusLabels];
}

or

function hasOwn<T extends object, K extends PropertyKey>(
  obj: T,
  key: K
): key is K & keyof T {
  return Object.hasOwn(obj, key);
}

then use:

if (hasOwn(statusLabels, status)) {
  return statusLabels[status];
}

This works, but it forces every project to define its own wrapper around a standard JavaScript API.

3. What workarounds are you using in the meantime?

  • type assertions with as keyof typeof obj
  • custom hasOwn type guard helpers
  • switching to Map in places where plain objects are otherwise enough
  • using in, although in checks the prototype chain and is not equivalent to Object.hasOwn()

The preferred runtime API is already Object.hasOwn(), so it would be useful if TypeScript's control-flow analysis understood it more directly.

Metadata

Metadata

Assignees

No one assigned

    Labels

    DuplicateAn existing issue was already created

    Type

    No type
    No fields configured for issues without a type.

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions