ChatGPT解决这个技术问题 Extra ChatGPT

TypeScript:Object.keys 返回字符串 []

使用 Object.keys(obj) 时,返回值为 string[],而我想要 (keyof obj)[]

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

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

我有错误:

元素隐含地具有“任何”类型,因为类型 '{ a: number; b:号码; }' 没有索引签名。

带有 strict: true 的 TypeScript 3.1。 Playground:here,请选中 Options 中的所有复选框以激活 strict: true

不要认为你可以比类型断言做得更好(Object.keys(v) as Array<keyof typeof v>)定义就是这样
我认为使用我的 object-typed 包比在任何地方都使用这种类型断言更清晰:ObjectTyped.keys(v)。只需 npm i object-typed 然后导入它。

D
Devin Rhode

Object.keys 返回一个 string[]。这是本 issue 中所述的设计

这是故意的。 TS 中的类型是开放式的。因此,keysof 可能会少于您在运行时获得的所有属性。

有几种解决方案,最简单的一种是只使用类型断言:

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])[]);

您还可以在 Object 中为 keys 创建一个别名,该别名将返回您想要的类型:

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])[]);

你能解释一下为什么“所以 keyof 可能会少于你在运行时获得的所有属性”。 ?
@user2010955 是来自 GitHub 线程的引用,我的理解是,由于您可以向 JS 中的任何对象添加更多属性,因此 keyof 运算符不会返回与 Objects.keys 相同的内容
还有this quote from Anders展示了他们的思考过程。
正如@EmileBergeron 指出的那样,您可以考虑定义自己的本地函数,而不是扩展 Object 的本机属性。例如:function getTypedKeys<T>(obj: T): Array<keyof T> { return Object.keys(obj) as Array<keyof typeof obj>; }。然后,在您通常写 Object.keys(obj) 的地方,您改为写 getTypedKeys(obj)
B
Ben Carp

基于 Titian Cernicova-Dragomir 的回答和评论

仅当您知道您的对象没有额外的属性时才使用类型断言(例如对象文字而不是对象参数的情况)。

显式断言

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

隐藏断言

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

使用 getKeys 而不是 Object.keysgetKeys 是对 Object.keys 的引用,但返回是按字面输入的。

讨论

TypeScript 的核心原则之一是类型检查侧重于值的形状。 (参考)

interface SimpleObject {
   a: string 
   b: string 
}

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

x 有资格作为 SimpleObject,因为它有它的形状。这意味着当我们看到 SimpleObject 时,我们知道它具有属性 ab,但它也可能具有其他属性。

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

someFunction(x)

让我们看看如果默认情况下我们按照 OP“字面意思”的需要键入 Object.keys 会发生什么:

我们会得到 typeof k"a"|"b"。迭代时,实际值为 abc。 Typescript 通过将 k 键入为字符串来保护我们免受此类错误的影响。

类型断言正是针对这种情况的——当程序员有额外的知识时。如果您知道 obj 没有额外的属性,则可以使用文字类型断言。


应该选择答案!
这实际上非常清晰简洁......谢谢。
@DevinGRhode,两种解决方案都是一样的,而且都是合法的。不同之处在于 getKeys 对用户隐藏了断言。因此,它创建了一个更易于使用的工具。唯一的问题是您可能不希望函数的唯一功能是类型断言,这就是我倾向于更明确的解决方案的原因。
@DevinGRhode,您的意思是我应该将我的解决方案移到讨论之上吗?
@DevinGRhode,我重视您的反馈。我根据您的建议编辑了我的答案。
J
Jeff Stock

请参阅https://github.com/microsoft/TypeScript/issues/20503

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

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

将保留键类型而不是 string[]


缺少 window.BetterObject = {keys: Object.keys} 的实际 javascript 定义
D
Devin Rhode

我完全不同意 Typescript 团队的决定...
按照他们的逻辑,Object.values 应该始终返回任何内容,因为我们可以在运行时添加更多属性...

我认为正确的方法是创建具有可选属性的接口并在您进行时设置(或不设置)这些属性......

所以我只是简单地在本地覆盖了 ObjectConstructor 接口,方法是在我的项目中添加一个声明文件(又名:whatever.d.ts),其中包含以下内容:


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;

笔记:

原始类型(对象)的 Object.keys/Object.entries 将返回 never[] 和 [never, never][] 而不是正常的 string[] 和 [string, any][]。如果有人知道解决方案,请随时在评论中告诉我,我将编辑我的答案

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][]

所以,请谨慎使用...


我喜欢你前进的方向。我希望我们不需要为每个签名重复 JSDoc 签名描述。我认为有一种方法可以解决 never 的情况,但我不确定。
是的,我创建的速度非常快,它支持我团队的所有用例,我可以稍后再试一次,但没有任何承诺......
我清理了一些重复的评论。我很好奇您能否解释拥有多个签名的最终结果是什么。 (另外,重复jsdoc注释有必要吗?我以前没这样用过)
interface ObjectConstructor extends Omit<ObjectConstructor, 似乎不正确/错误,因为 ObjectContructor 递归引用自身
当您声明 ObjectConstructor 时,将有 2 个对 ObjectConstructor 的引用,新的和旧的。由于我在接口的声明状态下使用 ObjectConstructor,它将引用旧接口,而不是它本身。我的意思是....至少我是这么认为的:P
D
Devin Rhode

伟大的斯科特!

现在有一个不错的 npm 包:

npm install object-typed --save

用法:

import { ObjectTyped } from 'object-typed'

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

这将返回一个 ['a'] 类型的数组

(不是开发依赖项,因为您将其import 放入您的应用程序 src


因为它实际上是 1 个文件,所以我没有为此设置 github 存储库。如果您有问题,请在此处发表评论。这是 npm 链接:npmjs.com/package/object-typed
我最近发现了这一点:github.com/sindresorhus/ts-extras/blob/main/source/… 并且正在对它进行测试。 :) 我假设这些人比我有更多的 TS 经验/知识,这通常可能是一个更好的库方法/功能。
ts-extras 的一个缺点是它不能在 cjs 节点脚本中无缝工作。
D
Devin Rhode

您可以使用 Extract 实用程序类型使您的参数仅符合 objkeys 字符串(因此,在您编码时忽略任何数字/符号)。

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`
})

尽管如此,大多数开发人员可能会认为将数字作为键是一个糟糕的设计决策/要修复的错误。


这似乎不起作用? typescriptlang.org/play/#code/…
我不能完全理解这一点。我想,对象键不必是字符串,它们可以是符号或数字。我认为对于 99% 的对象键是字符串的情况,使用 Extract 有什么意义吗?
这是提取签名:type Extract<T, U> = T extends U ? T : never 和描述:Extract from T those types that are assignable to U
我懂了。 Extract 保证 obj 的 key 是一个字符串。所以过滤我们的理论符号和 TS 不能保证的数字不会在运行时出现,可能是由于突变。
我现在看到了 Extract 的实用性。这是一个不同的示例:typescriptlang.org/play?#code/…
A
Aaron Newman

这是我用于以类型安全方式复制对象的模式。它使用字符串缩小,因此编译器可以推断键实际上是类型。这通过一个类进行了演示,但可以在接口或相同形状的匿名类型之间工作。

它有点冗长,但可以说比公认的答案更直接。如果您必须在多个地方进行复制操作,它确实可以节省打字。

请注意,如果类型不匹配,这抛出错误,但如果 thingNum 中缺少字段,不会抛出错误.所以这可能是 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

作为一种可能的解决方案,您可以在对象上使用 for..in 进行迭代:

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

正如您所说,虽然这不起作用:

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

对于 for..in,在给定的操场链接中,k 只是一个字符串,一定有一些 TS 错误导致现在失败。当前失败,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)
在 TS 4.1.2 中,for..in 循环似乎与当前的 Object.keys no 不同,对于它们,你最终只会得到 key: string