【问题标题】:TypeScript: Object.keys return string[]TypeScript:Object.keys 返回字符串 []
【发布时间】:2019-03-22 05:28:57
【问题描述】:

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

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

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

我有错误:

元素隐含了一个'any'类型,因为type '{ a: number; b:号码; }' 没有索引签名。

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

【问题讨论】:

  • 别以为你能比类型断言做得更好(Object.keys(v) as Array<keyof typeof v>)定义就是这样

标签: typescript


【解决方案1】:

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 运算符不会返回与 @ 相同的内容987654331@
  • 还有this quote from Anders展示了他们的思考过程。
  • 正如@EmileBergeron 指出的那样,您可以考虑定义自己的本地函数,而不是扩展Object 的本机属性。例如:function getTypedKeys&lt;T&gt;(obj: T): Array&lt;keyof T&gt; { return Object.keys(obj) as Array&lt;keyof typeof obj&gt;; }。然后,在你通常写Object.keys(obj) 的地方,你改为写getTypedKeys(obj)
【解决方案2】:

基于 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.keysgetKeysObject.keys 的引用,但返回是按字面输入的。

讨论

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

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,我重视您的反馈。我根据您的建议编辑了我的答案。
【解决方案3】:

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 定义
【解决方案4】:

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

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

所以我只是简单地在本地覆盖了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][]。如果有人知道解决方案,请随时在 cmets 中告诉我,我将编辑我的答案

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 的情况,但我不确定。
  • 是的,我创建的速度非常快,它支持我团队的所有用例,我可以稍后再试一次,但没有任何承诺......
  • 我清理了一些重复的 cmets。我很好奇您能否解释拥有多个签名的最终结果是什么。 (还有,重复jsdoc cmets有必要吗?我之前没这么用过)
  • interface ObjectConstructor extends Omit&lt;ObjectConstructor, 似乎不正确/错误,因为 ObjectContructor 递归地引用自身
  • 当您声明 ObjectConstructor 时,会有 2 个对 ObjectConstructor 的引用,新的和旧的。由于我在接口的声明状态下使用 ObjectConstructor,它将引用旧接口,而不是它本身。我的意思是....至少我是这么认为的:P
【解决方案5】:

伟大的斯科特!

现在有一个不错的 npm 包:

npm install object-typed --save

用法:

import { ObjectTyped } from 'object-typed'

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

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

(不是开发依赖项,因为您将 import 它添加到您的应用程序中 src

【讨论】:

  • 因为它实际上是 1 个文件,所以我没有为此设置 github repo。如果您有问题,请在此处发表评论。这是 npm 链接:npmjs.com/package/object-typed
【解决方案6】:

您可以使用 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&lt;T, U&gt; = T extends U ? T : never 和描述:Extract from T those types that are assignable to U
  • 哦,我明白了。 Extract 保证 obj 的 key 是一个字符串。所以过滤我们的理论符号和 TS 不能保证的数字不会在运行时出现,可能是由于突变。
  • 我现在看到了Extract 的实用性。这是一个不同的例子:typescriptlang.org/play?#code/…
【解决方案7】:

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

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

请注意,如果类型不匹配,这抛出错误,这是你想要的,但不会在缺少字段时抛出错误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

【讨论】:

    【解决方案8】:

    作为一种可能的解决方案,您可以在对象上使用 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 循环似乎no 与当前的 Object.keys 不同,对于它们,你最终会得到 key: string
    猜你喜欢
    • 1970-01-01
    • 2020-08-07
    • 2012-05-03
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    相关资源
    最近更新 更多