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

Expanding unfamiliar categories

Currently, I am working with Gutenberg blocks in a headless manner. Each Gutenberg block is defined by the following structure: type Block = { name: string; className?: string; key?: string | number; clientId: string; innerBlocks: Block ...

Visual Studio Code unable to locate source maps for typescript debugging

Looking for some help debugging a basic Hello World TypeScript file. Whenever I try to set a breakpoint, it seems like VS Code is having trouble locating the source map, even though it's saved in the same directory. I'm using Chrome as my browser ...

Leverage Async Await for Setting Response Data in TypeScript

Currently, I am making multiple API requests with different data and storing all the responses in an array. Then, I am using .map to map the response array to my original array to update the data. However, it seems like the process is not working correctly ...

Error: The npm-link library encountered an invalid hook call

Problem Description: I am working on developing a package named eformless. To set up the package, I utilized CRA to create a directory named sandbox where I linked the package. However, I keep encountering an error when attempting to launch the sand ...

The error message "TypeError: Unable to access the 'getFullWidth' property of an undefined value when using TSLint and TypeScript" was

I have been using Dan Wahlin's tutorials and online examples to set up Gulp and Typescript, but I am facing an issue with the tslint() function. The problem occurs in my code as follows: node_modules\tslint\lib\language\walker&bso ...

What is causing the TypeScript error in the MUI Autocomplete example?

I am attempting to implement a MUI Autocomplete component (v5.11) using the example shown in this link: import * as React from 'react'; import TextField from '@mui/material/TextField'; import Autocomplete from '@mui/material/Autoco ...

Creating a definition for the use of sweet alerts within a service and incorporating them through

Implementing sweet alert for displaying alert messages in angularJS2/typescript. Due to the repetitive nature of this code in different parts of the application, a service was created. @Injectable() export class AlertMessageService { constructor(pr ...

The name is not found when using attribute, but it is found when using extends

Lately, I've encountered difficulties with creating large TypeScript modules, and there's one thing that has been puzzling me. Specifically, the following scenario doesn't seem to work: // file A.ts export = class A { } // file main.ts imp ...

Making an Angular 6 HTTP GET call using HTTP-Basic authentication

When attempting to access a URL that requires Basic Authentication, and returns JSON data, what is the proper way to include my username and password in the following HTTP request? private postsURL = "https://jsonExample/posts"; getPosts(): Observable& ...

Unclear value of button when being passed

In my Welcome.html file, I am attempting to send the value of a button to a function that simply logs that value. This function is located in a functions class that has been imported into my welcome.ts file. <ion-content padding id="page1"> <h1 ...

Build a unique array of identifiers extracted from an object

I am seeking advice on how to extract an array of IDs values by iterating through an object in React JS. const initialState = useMemo(()=> { return dataTable.filter(result => favorites.some(favorite => result.id === favorite.id)) },[ ...

Creating a ref with Typescript and styled-components: A comprehensive guide

Trying to include a ref into a React component looks like this: const Dashboard: React.FC = () => { const [headerHeight, setHeaderHeight] = useState(0); const headerRef = React.createRef<HTMLInputElement>(); useEffect(() => { // @ts ...

To ensure the specificity selector for material UI in React, it is essential to include an empty CSS definition

The styling for the unselected toggle button is working smoothly. However, when you don't define an empty class selector, the style of the selected toggle button does not show: ./App.js import * as React from "react"; { render ...

Unable to expand the dropdown button collection due to the btn-group being open

Having trouble with the .open not working in Bootstrap 4.3 after adding the btn-group class to open the dropdown... I am looking for a way to load the dropdown without using JavaScript from Bootstrap. This is the directive I am trying to use: @Host ...

What is the best way to trigger a mat-menu to open with just one click, while also automatically closing any other open menus at

I've encountered an issue where if there are multiple menus in the header, opening a menu for the first time works fine. However, if a menu is already open and I try to open another one, it doesn't work as expected. It closes the previously opene ...

Ensure all promises are resolved inside of for loops before moving on to the next

Within my angular 11 component, I have a process that iterates through elements on a page and converts them from HTML to canvas to images, which are then appended to a form. The problem I am encountering is that the promise always resolves after the ' ...

What is the process for obtaining a flattened tuple type from a tuple comprised of nested tuples?

Suppose I have a tuple comprised of additional tuples: type Data = [[3,5,7], [4,9], [0,1,10,9]]; I am looking to develop a utility type called Merge<T> in such a way that Merge<Data> outputs: type MergedData = Merge<Data>; // type Merged ...

Incorporating a new function into a TypeScript (JavaScript) class method

Is it possible to add a method to a method within a class? class MyClass { public foo(text: string): string { return text + ' FOO!' } // Looking for a way to dynamically add the method `bar` to `foo`. } const obj = new MyCl ...

Ways to access the values of checkboxes that are initially checked by default

Recently, I was working on a project that involved multiple checkboxes. My goal was to have the checkboxes pre-checked with values in the form (using reactive form). Users should be able to unselect the boxes as they wish and the data would be stored accor ...

Using Angular BehaviorSubject in different routed components always results in null values when accessing with .getValue or .subscribe

I am facing an issue in my Angular application where the JSON object saved in the service is not being retrieved properly. When I navigate to another page, the BehaviorSubject .getValue() always returns empty. I have tried using .subscribe but without succ ...