ChatGPT解决这个技术问题 Extra ChatGPT

TypeScript: Object.keys return string[]

When using Object.keys(obj), the return value is a string[], whereas I want a (keyof obj)[].

const v = {
    a: 1,
    b: 2
}

Object.keys(v).reduce((accumulator, current) => {
    accumulator.push(v[current]);
    return accumulator;
}, []);

I have the error:

Element implicitly has an 'any' type because type '{ a: number; b: number; }' has no index signature.

TypeScript 3.1 with strict: true. Playground: here, please check all checkboxes in Options to activate strict: true.

Don't think you can do better then a type assertion (Object.keys(v) as Array<keyof typeof v>) the definition is what it is
I think using my object-typed package is definition cleaner than doing this type assertion everywhere: ObjectTyped.keys(v). Just npm i object-typed and then import it.

D
Devin Rhode

Object.keys returns a string[]. This is by design as described in this issue

This is intentional. Types in TS are open ended. So keysof will likely be less than all properties you would get at runtime.

There are several solution, the simplest one is to just use a type assertion:

const v = {
    a: 1,
    b: 2
};

var values = (Object.keys(v) as Array<keyof typeof v>).reduce((accumulator, current) => {
    accumulator.push(v[current]);
    return accumulator;
}, [] as (typeof v[keyof typeof v])[]);

You can also create an alias for keys in Object that will return the type you want:

export const v = {
    a: 1,
    b: 2
};

declare global {
    interface ObjectConstructor {
        typedKeys<T>(obj: T): Array<keyof T>
    }
}
Object.typedKeys = Object.keys as any

var values = Object.typedKeys(v).reduce((accumulator, current) => {
    accumulator.push(v[current]);
    return accumulator;
}, [] as (typeof v[keyof typeof v])[]);

Could you explain me why "So keysof will likely be less than all properties you would get at runtime." ?
@user2010955 that is a quote from the GitHub thread, My understanding of it is that since you can add more properties to any object to JS, the keyof operator will not return the same things as Objects.keys
There's also this quote from Anders which shows their thinking process.
As @EmileBergeron pointed out, you might consider defining your own local function rather than extending the native properties of Object. For example: function getTypedKeys<T>(obj: T): Array<keyof T> { return Object.keys(obj) as Array<keyof typeof obj>; }. Then, where you would normally write Object.keys(obj), you instead write getTypedKeys(obj).
B
Ben Carp

Based on Titian Cernicova-Dragomir answer and comment

Use type assertion only if you know that your object doesn't have extra properties (such is the case for an object literal but not an object parameter).

Explicit assertion

Object.keys(obj) as Array<keyof typeof obj>

Hidden assertion

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

Use getKeys instead of Object.keys. getKeys is a ref to Object.keys, but the return is typed literally.

Discussions

One of TypeScript’s core principles is that type checking focuses on the shape that values have. (reference)

interface SimpleObject {
   a: string 
   b: string 
}

const x = {
   a: "article", 
   b: "bridge",
   c: "Camel" 
}

x qualifies as a SimpleObject because it has it's shape. This means that when we see a SimpleObject, we know that it has properties a and b, but it might have additional properties as well.

const someFunction = (obj: SimpleObject) => {
    Object.keys(obj).forEach((k)=>{
        ....
    })
}

someFunction(x)

Let's see what would happen if by default we would type Object.keys as desired by the OP "literally":

We would get that typeof k is "a"|"b". When iterating the actual values would be a, b, c. Typescript protects us from such an error by typing k as a string.

Type assertion is exactly for such cases - when the programmer has additional knowledge. if you know that obj doesn't have extra properties you can use literal type assertion.


Should be selected answer!
This was actual very clear and concise ...thank you.
@DevinGRhode, both solutions do the same, and are both legit. The difference is that getKeys hides assertion from the user. Therefor it creates an easier to use tool. The only thing is you might not expect the only functionality of a function to be type assertion which is why I lean towards the more explicit solution.
@DevinGRhode, Do you mean I should move my solution above the discussion?
@DevinGRhode, I value your feedback. I edited my answer per your suggestions.
J
Jeff Stock

See https://github.com/microsoft/TypeScript/issues/20503.

declare const BetterObject: {
  keys<T extends {}>(object: T): (keyof T)[]
}

const icons: IconName[] = BetterObject.keys(IconMap)

Will retain type of keys instead of string[]


Missing actual javascript definition of window.BetterObject = {keys: Object.keys}
D
Devin Rhode

I completely disagree with Typescript's team's decision...
Following their logic, Object.values should always return any, as we could add more properties at run-time...

I think the proper way to go is to create interfaces with optional properties and set (or not) those properties as you go...

So I simply overwrote locally the ObjectConstructor interface, by adding a declaration file (aka: whatever.d.ts) to my project with the following content:


declare interface ObjectConstructor extends Omit<ObjectConstructor, 'keys' | 'entries'> {
    /**
     * Returns the names of the enumerable string properties and methods of an object.
     * @param obj Object that contains the properties and methods. This can be an object that you created or an existing Document Object Model (DOM) object.
     */
    keys<O extends any[]>(obj: O): Array<keyof O>;
    keys<O extends Record<Readonly<string>, any>>(obj: O): Array<keyof O>;
    keys(obj: object): string[];

    /**
     * Returns an array of key/values of the enumerable properties of an object
     * @param obj Object that contains the properties and methods. This can be an object that you created or an existing Document Object Model (DOM) object.
     */
    entries<T extends { [K: Readonly<string>]: any }>(obj: T): Array<[keyof T, T[keyof T]]>
    entries<T extends object>(obj: { [s: string]: T } | ArrayLike<T>): [string, T[keyof T]][];
    entries<T>(obj: { [s: string]: T } | ArrayLike<T>): [string, T][];
    entries(obj: {}): [string, any][];
}

declare var Object: ObjectConstructor;

Note:

Object.keys/Object.entries of primitive types (object) will return never[] and [never, never][] instead of the normal string[] and [string, any][]. If anyone knows a solutions, please, feel free to tell me in the comments and I will edit my answer

const a: {} = {};
const b: object = {};
const c: {x:string, y:number} = { x: '', y: 2 };

// before
Object.keys(a) // string[]
Object.keys(b) // string[]
Object.keys(c) // string[]
Object.entries(a) // [string, unknown][]
Object.entries(b) // [string, any][]
Object.entries(c) // [string, string|number][]

// after
Object.keys(a) // never[]
Object.keys(b) // never[]
Object.keys(c) // ('x'|'y')[]
Object.entries(a) // [never, never][]
Object.entries(b) // [never, never][]
Object.entries(c) // ['x'|'y', string|number][]

So, use this with caution...


I like the direction you are headed. I'd hope that we don't need to repeat the JSDoc signature description for each signature. I think there is a way to fix the never situation, but I'm not sure off hand.
Yeah, I created this really fast and it supports all of my team's use cases, I can give it another try latter on but no promises there...
I cleaned up some of the repeating comments. I'm curious if you could explain what the end result of having the multiple signatures is. (Also, is repeating the jsdoc comments is necessary? I haven't used it like this before)
the interface ObjectConstructor extends Omit<ObjectConstructor, seems incorrect/wrong because ObjectContructor recursively references itself
When you declare ObjectConstructor there will be 2 references to ObjectConstructor, the new one and the old one. Since Im using ObjectConstructor at the declaration state of the interface, it will references the old interface, not itself. I mean.... at least I think so :P
D
Devin Rhode

Great Scott!

There's a nice npm package now:

npm install object-typed --save

Usage:

import { ObjectTyped } from 'object-typed'

ObjectTyped.keys({ a: 'b' })

This will return an array of type ['a']

(Not a dev dependency, since you import it into your app src)


Since it's literally 1 file, turns out I don't have a github repo setup for this. If you have issues, please comment here. Here's npm link: npmjs.com/package/object-typed
I recently discovered this: github.com/sindresorhus/ts-extras/blob/main/source/… and am kicking the tires on it. :) I assume that these people have more TS experience/knowledge than me, and this may generally be a better library method/function to use.
One downside to ts-extras is that it won't work seamlessly in cjs node scripts.
D
Devin Rhode

You can use the Extract utility type to conform your param to only the keys of obj which are strings (thus, ignoring any numbers/symbols when you are coding).

const obj = {
  a: 'hello',
  b: 'world',
  1: 123 // 100% valid
} // if this was the literal code, you should add ` as const` assertion here

// util
type StringKeys<objType extends {}> = Array<Extract<keyof objType, string>>

// typedObjKeys will be ['a', 'b', '1'] at runtime
// ...but it's type will be Array<'a' | 'b'>
const typedObjKeys = Object.keys(obj) as StringKeys<typeof obj>

typedObjKeys.forEach((key) => {
  // key's type: 'a' | 'b'
  // runtime: 'a', 'b', AND '1'
  const value = obj[key]
  // value will be typed as just `string` when it's really `string | number`
})

All that said, most developers would probably consider having numbers as keys a poor design decision/bug to be fixed.


this doesn't seem to work? typescriptlang.org/play/#code/…
I can't quite make sense of this. I guess, object keys don't have to be strings, they could be symbols or numbers. I think for 99% of cases where object keys ARE strings, is there any point to using Extract?
This is the Extract signature: type Extract<T, U> = T extends U ? T : never and description: Extract from T those types that are assignable to U
Oh, I see. The Extract guarantee's that keyof the obj is a string. So filters our theoretical symbols and numbers that TS can't guarantee don't somehow don't appear at runtime, perhaps due to mutation.
I see the utility of Extract now. Here's a different example: typescriptlang.org/play?#code/…
A
Aaron Newman

Here is a pattern I use for copying objects in a typesafe way. It uses string narrowing so the compiler can infer the keys are actually types. This was demonstrated with a class, but would work with/between interfaces or anonymous types of the same shape.

It is a bit verbose, but arguably more straightforward than the accepted answer. If you have to do the copying operation in multiple places, it does save typing.

Note this will throw an error if the types don't match, which you'd want, but doesn't throw an error if there are missing fields in thingNum. So this is maybe a disadvantage over Object.keys.


    class thing {
    a: number = 1;
    b: number = 2;
    }
    type thingNum = 'a' | 'b';
    const thingNums: thingNum[] = ['a', 'b'];
    
    const thing1: thing = new thing();
    const thing2: thing = new thing(); 
...
    
    thingNums.forEach((param) => {
        thing2[param] = thing1[param];
    });

playground link


D
Devin Rhode

As a possible solution, you can iterate using for..in over your object:

for (const key in myObject) {
  console.log(myObject[key].abc); // works, but `key` is still just `string`
}

While this, as you said, would not work:

for (const key of Object.keys(myObject)) {
  console.log(myObject[key].abc); // doesn't!
}

With for..in, in the given playground link, k is just a string, there must have been some TS error that is making this now fail. It currently fails with a red underline on obj[k]: Element implicitly has an 'any' type because expression of type 'string' can't be used to index type 'Obj'. No index signature with a parameter of type 'string' was found on type 'Obj'.(7053)
With TS 4.1.2, for..in loop seems no different than the current Object.keys, with both of them, you just end up with key: string