What is the reason for Object.keys not returning a keyof type in TypeScript?

Wondering why Object.keys(x) in TypeScript doesn't return the type Array<keyof typeof x>? It seems like a missed opportunity as Object.keys outputs that by default. Should I report this on their GitHub repo, or should I just submit a pull request to correct it?

Answer №1

The decision to maintain the current return type (string[]) is deliberate. But why?

Let's look at an example type:

interface Point {
    x: number;
    y: number;
}

Suppose you have a function like this:

function fn(k: keyof Point) {
    if (k === "x") {
        console.log("X axis");
    } else if (k === "y") {
        console.log("Y axis");
    } else {
        throw new Error("This is impossible");
    }
}

Here's a question for you:

In a well-typed program, can a legitimate call to fn result in an error?

The expected answer is obviously "No". So, what does this have to do with Object.keys?

Now, consider the following code snippet:

interface NamedPoint extends Point {
    name: string;
}

const origin: NamedPoint = { name: "origin", x: 0, y: 0 };

Note that as per TypeScript's type system, all instances of NamedPoint are valid instances of Point.

Let's add a bit more code:

function doSomething(pt: Point) {
    for (const k of Object.keys(pt)) {
        // A proper call only if Object.keys(pt) returns (keyof Point)[]
        fn(k);
    }
}
// Throws an exception
doSomething(origin);

Our perfectly typed program just threw an exception!

There seems to be an issue here! By using keyof T, we've breached the assumption that keyof T constitutes an exhaustive list. This is because having a reference to an object doesn't guarantee that the type of the reference is not a supertype of the type of the value.

In essence, at least one of the following statements must be false:

  1. keyof T represents an exhaustive list of keys of T
  2. A type with additional properties is always a subtype of its base type
  3. It is permissible to alias a subtype value with a supertype reference
  4. Object.keys should return keyof T

Discarding point 1 renders keyof almost useless since it suggests that keyof Point could be something other than "x" or "y".

Getting rid of point 2 would dismantle TypeScript's type system completely.

Similarly, abolishing point 3 would also demolish TypeScript's type system entirely.

However, discarding point 4 is acceptable and prompts you, the programmer, to contemplate whether the object you're dealing with might be an alias for a subtype of the intended target.

The hypothetical solution to make this scenario legitimate without contradiction lies in Exact Types, which would enable the declaration of a new distinct type exempt from point #2. With this feature, it might be viable for Object.keys to solely represent keyof T for declared exact types of T.


Addendum: What about generics?

Some individuals suggested that Object.keys could appropriately return keyof T if the argument was generic. However, that assertion is inaccurate. Here's why:

class Holder<T> {
    value: T;
    constructor(arg: T) {
        this.value = arg;
    }

    getKeys(): (keyof T)[] {
        // Assumed to be correct
        return Object.keys(this.value);
    }
}
const MyPoint = { name: "origin", x: 0, y: 0 };
const h = new Holder<{ x: number, y: number }>(MyPoint);
// 'name' value assigned to 'x' | 'y' variable
const v: "x" | "y" = (h.getKeys())[0];

Or consider this example, where explicit type arguments are unnecessary:

function getKey<T>(x: T, y: T): keyof T {
    // Believed to be feasible
    return Object.keys(x)[0];
}
const obj1 = { name: "", x: 0, y: 0 };
const obj2 = { x: 0, y: 0 };
// 'name' value inhabits 'x' | 'y' typed variable
const s: "x" | "y" = getKey(obj1, obj2);

Answer №2

If you are certain that the object you are working with does not have any additional properties, you can use the following workaround:

const obj = {x: 3, y: 4}
const objKeys = Object.keys(obj) as Array<keyof typeof obj>
// objKeys now holds type ("x" | "y")[]

To make things more efficient, you can move this code snippet to a function:

const getKeys = <T>(obj: T) => Object.keys(obj) as Array<keyof T>

const newObj = {x: 3, y: 4}
const newKeys = getKeys(newObj)
// newKeys now holds type ("x" | "y")[]

For your convenience, here is an example of using Object.entries, referenced from a GitHub issue discussing why this is not the default behavior:

type Pairs<T> = {
  [K in keyof T]: [K, T[K]]
}[keyof T][]

function entries<T>(obj: T): Pairs<T> {
  return Object.entries(obj) as any;
}

Answer №3

This is currently the top search result on Google for this particular issue, so I thought it would be valuable to provide some guidance on how to move forward.

These strategies have been compiled from extensive discussions found on various issue pages, which are linked in other responses and comment sections.

Let's say you were dealing with code similar to this:

const obj = {};
Object.keys(obj).forEach((key) => {
  obj[key]; // code that needs revising
});

Here are a few approaches to consider:

  1. If you only require values and not keys, utilize .entries() or .values() instead of iterating over the keys.

    const obj = {};
    Object.values(obj).forEach(value => value);
    Object.entries(obj).forEach([key, value] => value);
    
  2. Create a helper function:

    function keysOf<T extends Object>(obj: T): Array<keyof T> {
      return Array.from(Object.keys(obj)) as any;
    }
    
    const obj = { a: 1; b: 2 };
    keysOf(obj).forEach((key) => obj[key]); // key type: "a" | "b"
    
  3. Re-cast your type (useful for minimizing code rewrites)

    const obj = {};
    Object.keys(obj).forEach((_key) => {
      const key = _key as keyof typeof obj;
      obj[key];
    });
    

The most suitable approach for your project will depend on your specific requirements and preferences.

Answer №4

One potential answer

function checkName<W extends string, T extends Record<W, any>>(obj: T) {
  return (name: string): name is keyof T & W =>
    obj.hasOwnProperty(name);
}

const objectKeys = Object.keys(x).filter(checkName(x));

Answer №5

I also encountered a similar problem and came up with a solution by writing some typed functions.

Realizing that Object.keys and Object.entries always return keys as strings, I decided to define a new type called ToStringKey:

/**
 * Retrieves the names of the _typed_ enumerable string properties and methods from an object.
 *
 * Note: Using Object.keys with a specific type could result in inconsistencies between type-checking and runtime behavior.
 * This function is ideal when you are certain about the object's keys.
 */
export const getTypedKeys = Object.keys as <T extends object>(
  obj: T
  // Employing `ToStringKey` because Object.keys returns all keys as strings.
) => Array<ToStringKey<T>>;

/**
 * Obtains an array of _typed_ values from the enumerable properties of an object.
 */
export const getTypedValues = Object.values as <T extends object>(obj: T) => Array<T[keyof T]>;

/**
 * Fetches an array of _typed_ key/values from the enumerable properties of an object.
 *
 * Note: Confining Object.entries to a particular type may lead to discrepancies between type-checking and runtime behavior.
 * Utilize this function when you are sure about the object's keys.
 */
export const getTypedEntries = Object.entries as <T extends object>(
  obj: T
  // Adopting `ToStringKey` due to Object.entries returning all keys as strings.
) => Array<[ToStringKey<T>, T[keyof T]]>;

/**
 * Converts object keys into their respective string literal types.
 */
type ToStringKey<T> = `${Extract<keyof T, string | number>}`;

I would advise against defining these method types globally. Instead, create separate utility functions for them.

Although TypeScript can deduce and handle types, it cannot ascertain runtime-specific traits such as enumerability.

Answer №6

If you follow these steps, the issue will be resolved.

declare global {
  interface ObjectConstructor {
    keys<T>(o: T): (keyof T)[]
    // @ts-ignore
    entries<U, T>(o: { [key in T]: U } | ArrayLike<U>): [T, U][]
  }
}

The reason I included // @ts-ignore is because otherwise TypeScript would display this warning:

Type 'T' is not assignable to type 'string | number | symbol

If anyone has an alternative solution that eliminates the need for // @ts-ignore while still maintaining the dynamic nature of T, please share it in the comments.

In case this code causes issues, you can use the following workaround:

Object.tsKeys = function getObjectKeys<Obj>(obj: Obj): (keyof Obj)[] {
 return Object.keys(obj!) as (keyof Obj)[]
}
// @ts-ignore
Object.tsEntries = function getObjectEntries<U, T>(obj: { [key in T]: U }): [T, U][] {
 return Object.entries(obj!) as unknown as [T, U][]
}
declare global {
 interface ObjectConstructor {
   // @ts-ignore
   tsEntries<U, T>(o: { [key in T]: U }): [T, U][]
   tsKeys<T>(o: T): (keyof T)[]
 }
}

Similar questions

If you have not found the answer to your question or you are interested in this topic, then look at other similar questions below or use the search

Potential explanation for unexpected Typescript initialization error

I am encountering the compiler error The property 'options' is not initialized or assigned in the constructor. However, upon reviewing the code for the respective class, it is clear that this reported error does not accurately reflect the actual ...

The error of 'illegal invocation' occurs when attempting to use input setCustomValidity with TypeScript

I am new to the Typescript world and currently converting one of my first React applications. I am facing an issue while trying to set custom messages on input validation using event.target.setCustomValidity. I keep getting an 'Illegal invocation&apo ...

Implement Php gettext functionality in Typescript

I have a keen interest in AngularJs 2, and I am planning to incorporate it into my upcoming project. I would like to integrate i18n with Php gettext. In a previous project, I utilized Php gettext by embedding Php within Javascript code as shown below: // ...

What is the best way to effectively nest components with the Nebular UI Kit?

I'm currently facing an issue with nesting Nebular UI Kit components within my Angular app. Specifically, I am trying to nest a header component inside the home page component. The problem arises when the attributes take up the entire page by default, ...

What is the best way to detect object changes in typescript?

Having an object and the desire to listen for changes in order to execute certain actions, my initial approach in ES6 would have been: let members = {}; let targetProxy = new Proxy(members, { set: function (members, key, value) { console.log(k ...

Angular 14 Observables are not triggering resize events

There seems to be an issue here, as the code is not being triggered at all. The console log is not printing and this.width is not changing. constructor(private host: ElementRef, private zone: NgZone) {} public ngOnInit(): void { this.observer = new Re ...

There is no overload that fits this call (regarding a basic array retrieved from an api)

While attempting to utilize a .map function on a simple array (['a','b','c']) fetched using the useEffect hook, I encountered an error in TypeScript. The array elements rendered correctly when the code was executed and no erro ...

Issue with TypeScript: Difficulty accessing keys in a recursive manner

I've created a custom type that eliminates any nullish values when working with objects. export type ExcludeNullish<T> = Exclude<T, null | undefined>; export type ExcludeNullishKeys<T> = { [K in keyof T]-?: T[K] extends boolean | ...

Determine the type and create an instance of a fresh class

In my app, I have a function that handles all API requests. Any interaction I make goes through this function. I'm trying to set a specific return type for this function, but the return type is of a class. In order to use the methods of this class, I ...

Material-UI Slide component is encountering an issue where it is unable to access the style property of an undefined variable

Recently, I incorporated Material-UI Slide into my project and I'm curious about why the code functions correctly when written in this manner: {selectedItem && selectedItem.modal && selectedItem.modal.body ? ( selectedItem.modal.body.map((section ...

Error: module not found in yarn

In my yarn workspace, I have organized folders named public and server. While working with TypeScript in VS Code, I encounter an error message stating: Cannot find module 'x' Interestingly, even though the error persists, IntelliSense suggests ...

Struggling with Angular 5 Facebook authentication and attempting to successfully navigate to the main landing page

I have been working on integrating a register with Facebook feature into an Angular 5 application. Utilizing the Facebook SDK for JavaScript has presented a challenge due to the asynchronous nature of the authentication methods, making it difficult to redi ...

Why is it important to use linting for JavaScript and TypeScript applications?

Despite searching extensively on Stack Overflow, I have yet to find a comprehensive answer regarding the benefits of linting applications in Typescript and Javascript. Any insights or resources would be greatly appreciated. Linting has become second natur ...

What is the process for enabling Namespaces in CRA?

When creating a TypeScript React app, I used the following command: yarn create react-app my-app --template typescript This setup compiles my project using Babel and bundles it with webpack. Now, I want to utilize TypeScript namespaces, which are not nat ...

HashId plugin for Angular

Currently, I'm attempting to integrate into my Angular project built on the latest version. After discovering this definition file: // Type definitions for Hashids.js 1.x // Project: https://github.com/ivanakimov/hashids.node.js // Definitions by: ...

What is the process for upgrading TypeScript to the newest version using npm?

My current TypeScript version on my machine is 1.0.3.0 and I am looking to upgrade it to the latest version, which is 2.0. Could someone please guide me on how to accomplish this using npm? ...

How can the Singleton pattern be properly implemented in Typescript/ES6?

class Foo{ } var instance: Foo; export function getFooInstance(){ /* logic */ } or export class Foo{ private static _instance; private constructor(){}; public getInstance(){/* logic */} } // Use it like this Foo.getInstance() I would ...

Is there a convenient method for setting up and activating real-time TypeScript checking in Windows 10 using VS Code?

After successfully installing VS Code on my Windows 10 system, I decided to follow the instructions provided here. Upon completion, Node and NPM were also installed correctly. However, I noticed a gap in the setup instructions between installing TypeScrip ...

How can I create an Array of objects that implement an interface? Here's an example: myData: Array<myInterface> = []; However, I encountered an issue where the error message "Property 'xxxxxx' does not exist on type 'myInterface[]'" appears

Currently, I am in the process of defining an interface for an array of objects. My goal is to set the initial value within the component as an empty array. Within a file, I have created the following interface: export interface myInterface{ "pictur ...

How can I verify that the value entered in an input field matches a specific date format such as "MM/dd/YYYY" using Angular?

I need to validate if a given value matches a specific date format such as "MM/dd/YYYY." Typescript file onValChange(event: Date) { const datePipe = new DatePipe('en-US'); const val = datePipe.transform(event, 'MM/dd/yyyy'); ...