【问题标题】:Get keys of a Typescript interface as array of strings将 Typescript 接口的键作为字符串数组获取
【发布时间】:2017-10-10 02:39:54
【问题描述】:

我在Lovefield 中有很多表,以及它们各自的接口,它们有哪些列。

例子:

export interface IMyTable {
  id: number;
  title: string;
  createdAt: Date;
  isDeleted: boolean;
}

我想将此接口的属性名称放在这样的数组中:

const IMyTable = ["id", "title", "createdAt", "isDeleted"];

我无法直接基于接口IMyTable 创建对象/数组,这应该可以解决问题,因为我将动态获取表的接口名称。因此,我需要在接口中遍历这些属性并从中获取一个数组。

我如何实现这个结果?

【问题讨论】:

  • @andnik 标记的答案并不总是最好或正确的答案,而是适用于 OP 的 SELECTED 答案。当我问这个问题时,没有一个答案对我有用。之后我没有详细说明,所以请随时尝试这些答案并对其进行投票,以便其他人能够理解最常用的答案。谢谢。
  • 这个答案在我看来是最正确的,不幸的是没有提到更简单的方法:stackoverflow.com/questions/43909566/…

标签: javascript typescript


【解决方案1】:

不能。接口在运行时不存在。

解决方法

创建一个该类型的变量并在其上使用Object.keys ?

【讨论】:

  • 你的意思是这样的吗:var abc: IMyTable = {}; Object.keys(abc).forEach((key) => {console.log(key)});
  • 不,因为该对象上没有键。接口是 TypeScript 使用但在 JavaScript 中消失的东西,因此没有任何信息可以通知任何“反射”或“交互”。所有 JavaScript 都知道有一个空对象字面量。您唯一的希望是等待(或request that)TypeScript 包含一种将接口中的所有键生成数组或对象的方法到源代码中。或者,正如 Dan Def 所说,如果您可以使用一个类,那么您将在每个实例中以属性的形式定义键..
  • 如果这不起作用,为什么这个答案有人赞成?
  • downvote 原因:没有提到它不适用于可空值
  • 这最终不是一个很好的解决方案,因为您必须提供值。最好只保留一个键列表。
【解决方案2】:

也许为时已晚,但在 TypeScript 2.1 版中,您可以使用 keyof 来获取如下类型:

interface Person {
    name: string;
    age: number;
    location: string;
}

type K1 = keyof Person; // "name" | "age" | "location"
type K2 = keyof Person[];  // "length" | "push" | "pop" | "concat" | ...
type K3 = keyof { [x: string]: Person };  // string

来源:https://www.typescriptlang.org/docs/handbook/release-notes/typescript-2-1.html#keyof-and-lookup-types

【讨论】:

  • 感谢您的回答,但不确定它是否有助于某人从界面使用静态创建的类型。恕我直言,在大多数情况下,我们可以互换使用接口/类型。另外,这将需要为多个接口手动创建类型。但是,如果有人只需要从接口中获取类型,该解决方案看起来不错。
  • 我投了反对票,因为这并不能解决上述问题。目标是从一个类型中获取string[] 的列表;不要使用keyof MyType 获取密钥类型。
  • @CharlesCapps 这可以通过包装 keyof 轻松解决,如type K2 = (keyof Person)[]
  • 非常适合我。我想为我的应用程序的本地化增加一些安全性(使用 expo-localisation)。您的回答帮助我实现了它谢谢! (如果有人感兴趣,this article describes step-by-step)。
【解决方案3】:

我遇到了类似的问题:我有一个巨大的属性列表,我想将它们作为接口(编译时)和对象(运行时)。

注意:我不想写(用键盘输入)属性两次!干。


这里需要注意的一点是,接口在编译时是强制类型,而对象主要是在运行时。 (Source)

正如@derek 在another answer 中提到的,interfaceobject 的共同点可以是一个同时服务于 type

所以,TL;DR,以下代码应该可以满足需求:

class MyTableClass {
    // list the propeties here, ONLY WRITTEN ONCE
    id = "";
    title = "";
    isDeleted = false;
}

// ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~

// This is the pure interface version, to be used/exported
interface IMyTable extends MyTableClass { };

// ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~

// Props type as an array, to be exported
type MyTablePropsArray = Array<keyof IMyTable>;

// Props array itself!
const propsArray: MyTablePropsArray =
    Object.keys(new MyTableClass()) as MyTablePropsArray;

console.log(propsArray); // prints out  ["id", "title", "isDeleted"]


// ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~

// Example of creating a pure instance as an object
const tableInstance: MyTableClass = { // works properly!
    id: "3",
    title: "hi",
    isDeleted: false,
};

(Here是上面的代码在Typescript Playground中玩更多)

PS。如果您不想为类中的属性分配初始值并保持类型不变,则可以使用构造函数技巧:

class MyTableClass {
    // list the propeties here, ONLY WRITTEN ONCE
    constructor(
        readonly id?: string,
        readonly title?: string,
        readonly isDeleted?: boolean,
    ) {}
}

console.log(Object.keys(new MyTableClass()));  // prints out  ["id", "title", "isDeleted"] 

Constructor Trick in TypeScript Playground.

【讨论】:

  • propsArray 仅在您初始化密钥时才能访问。
  • 据我了解,当一个值具有任何值时,它就会被初始化。您的“构造函数技巧”具有误导性,因为您不能只用后者替换 MyTableClass 并期望在 propsArray 中接收键,因为未初始化的变量和类型在运行时被剥离。您总是必须为它们提供某种默认值。我发现用undefined 初始化它们是最好的方法。
  • @Aidin 感谢您的解决方案。我也想知道是否可以避免参数的初始化。如果我使用构造函数技巧,我将无法再创建扩展 MyTableClass 的接口 .. 你在 typescript 游乐场链接中的构造函数技巧是空的
  • @Flion,感谢您的关注。我刚刚更新了构造函数技巧的操场链接。看看它现在是否有效。
  • @MichaSchwab 我相信这可能是一个单独的问题。简而言之,TypeScript 是一个将您的 TypeScript 代码转换为 JavaScript 的编译器;然后是 Node/Browser 在运行时运行的 JavaScript。所以,期望一个编译类型的东西(TypeScript 类型),找到它进入运行时的方式在思维方式上是有点错误的。但是,您可以在方便时使用 vanilla JavaScript 的 typeof 运算符进行运行时类型检查。
【解决方案4】:

这是一个艰难的!谢谢大家的帮助。

我需要将接口的键作为字符串数组来简化 mocha/chai 脚本。不关心在应用程序中使用(还),所以不需要创建 ts 文件。感谢ford04 的帮助,他的解决方案above 是一个巨大的帮助,它完美地工作,没有编译器黑客。这是修改后的代码:

选项2:基于TS编译器API(ts-morph)的代码生成器

节点模块

npm install --save-dev ts-morph

keys.ts

注意:这里假设所有 ts 文件都位于 ./src 的根目录下,并且没有子文件夹,请相应调整

import {
  Project,
  VariableDeclarationKind,
  InterfaceDeclaration,
} from "ts-morph";

// initName is name of the interface file below the root, ./src is considered the root
const Keys = (intName: string): string[] => {
  const project = new Project();
  const sourceFile = project.addSourceFileAtPath(`./src/${intName}.ts`);
  const node = sourceFile.getInterface(intName)!;
  const allKeys = node.getProperties().map((p) => p.getName());

  return allKeys;
};

export default Keys;

用法

import keys from "./keys";

const myKeys = keys("MyInterface") //ts file name without extension

console.log(myKeys)

【讨论】:

    【解决方案5】:

    安全变体

    从具有安全编译时检查的接口创建一个数组或tuple 键需要一点创造力。类型在运行时被擦除,对象类型(无序、命名)无法转换为元组类型(有序、无名)without resorting to non-supported techniques

    与其他答案的比较

    这里提出的变体都考虑/触发一个编译错误,以防重复或缺失的元组项给定一个引用对象类型,如IMyTable。例如声明(keyof IMyTable)[] 的数组类型无法捕获这些错误。

    此外,它们不需要a specific library(最后一个变体使用ts-morph,我认为它是一个通用编译器包装器),发出一个元组类型as opposed to an object(只有第一个解决方案创建一个数组)或宽数组输入(比较theseanswers),最后输入don't need classes

    变体 1:简单类型数组

    // Record type ensures, we have no double or missing keys, values can be neglected
    function createKeys(keyRecord: Record<keyof IMyTable, any>): (keyof IMyTable)[] {
      return Object.keys(keyRecord) as any
    }
    
    const keys = createKeys({ isDeleted: 1, createdAt: 1, title: 1, id: 1 })
    // const keys: ("id" | "title" | "createdAt" | "isDeleted")[]
    

    +最简单的+-手动自动完成-数组,没有元组

    Playground

    如果您不喜欢创建记录,请查看this alternative with Set and assertion types


    变体 2:带有辅助函数的元组

    function createKeys<T extends readonly (keyof IMyTable)[] | [keyof IMyTable]>(
        t: T & CheckMissing<T, IMyTable> & CheckDuplicate<T>): T {
        return t
    }
    

    + tuple +- 具有自动完成功能的手动 +- 更高级、更复杂的类型

    Playground

    说明

    createKeys 会在编译时检查 by merging 函数参数类型以及其他断言类型,这些断言类型会针对不合适的输入发出错误。 (keyof IMyTable)[] | [keyof IMyTable] 是一个 "black magic" way,用于强制从被调用方推断元组而不是数组。或者,您可以在调用方使用const assertions / as const

    CheckMissing 检查,如果T 错过来自U 的键:

    type CheckMissing<T extends readonly any[], U extends Record<string, any>> = {
        [K in keyof U]: K extends T[number] ? never : K
    }[keyof U] extends never ? T : T & "Error: missing keys"
    
    type T1 = CheckMissing<["p1"], {p1:any, p2:any}> //["p1"] & "Error: missing keys"
    type T2 = CheckMissing<["p1", "p2"], { p1: any, p2: any }> // ["p1", "p2"]
    

    注意:T &amp; "Error: missing keys" 仅用于漂亮的 IDE 错误。你也可以写neverCheckDuplicates 检查双元组项:

    type CheckDuplicate<T extends readonly any[]> = {
        [P1 in keyof T]: "_flag_" extends
        { [P2 in keyof T]: P2 extends P1 ? never :
            T[P2] extends T[P1] ? "_flag_" : never }[keyof T] ?
        [T[P1], "Error: duplicate"] : T[P1]
    }
    
    type T3 = CheckDuplicate<[1, 2, 3]> // [1, 2, 3]
    type T4 = CheckDuplicate<[1, 2, 1]> 
    // [[1, "Error: duplicate"], 2, [1, "Error: duplicate"]]
    

    注意:有关元组中唯一项目检查的更多信息在 this post 中。使用TS 4.1,我们还可以在错误字符串中命名缺失的键——看看this Playground


    变体 3:递归类型

    TypeScript 4.1 版本正式支持conditional recursive types,这里也可以使用。但是,由于组合复杂性,类型计算成本很高 - 超过 5-6 个项目的性能会大幅下降。为了完整起见,我列出了这个替代方案 (Playground):

    type Prepend<T, U extends any[]> = [T, ...U] // TS 4.0 variadic tuples
    
    type Keys<T extends Record<string, any>> = Keys_<T, []>
    type Keys_<T extends Record<string, any>, U extends PropertyKey[]> =
      {
        [P in keyof T]: {} extends Omit<T, P> ? [P] : Prepend<P, Keys_<Omit<T, P>, U>>
      }[keyof T]
    
    const t1: Keys<IMyTable> = ["createdAt", "isDeleted", "id", "title"] // ✔
    

    + tuple +- 手动自动完成 + 没有辅助函数 -- 性能


    变体 4:代码生成器/TS 编译器 API

    这里选择ts-morph,因为它是original TS compiler API 的一个更简单的包装替代品。当然,你也可以直接使用编译器 API。我们看一下生成器代码:

    // ./src/mybuildstep.ts
    import {Project, VariableDeclarationKind, InterfaceDeclaration } from "ts-morph";
    
    const project = new Project();
    // source file with IMyTable interface
    const sourceFile = project.addSourceFileAtPath("./src/IMyTable.ts"); 
    // target file to write the keys string array to
    const destFile = project.createSourceFile("./src/generated/IMyTable-keys.ts", "", {
      overwrite: true // overwrite if exists
    }); 
    
    function createKeys(node: InterfaceDeclaration) {
      const allKeys = node.getProperties().map(p => p.getName());
      destFile.addVariableStatement({
        declarationKind: VariableDeclarationKind.Const,
        declarations: [{
            name: "keys",
            initializer: writer =>
              writer.write(`${JSON.stringify(allKeys)} as const`)
        }]
      });
    }
    
    createKeys(sourceFile.getInterface("IMyTable")!);
    destFile.saveSync(); // flush all changes and write to disk
    

    我们用tsc &amp;&amp; node dist/mybuildstep.js编译运行这个文件后,会生成一个文件./src/generated/IMyTable-keys.ts,内容如下:

    // ./src/generated/IMyTable-keys.ts
    const keys = ["id","title","createdAt","isDeleted"] as const;
    

    + 自动生成解决方案+ 可扩展为多个属性+ 没有辅助函数+ 元组- 额外的构建步骤- 需要熟悉编译器API

    【讨论】:

      【解决方案6】:

      TypeScript 2.3 开始(或者我应该说 2.4,就像在 2.3 中一样,此功能包含 a bug 已在 typescript@2.4-dev),你可以创建一个自定义的转换器来实现你想要的。

      其实我已经创建了这样一个自定义转换器,它可以实现以下功能。

      https://github.com/kimamula/ts-transformer-keys

      import { keys } from 'ts-transformer-keys';
      
      interface Props {
        id: string;
        name: string;
        age: number;
      }
      const keysOfProps = keys<Props>();
      
      console.log(keysOfProps); // ['id', 'name', 'age']
      

      不幸的是,自定义转换器目前并不那么容易使用。您必须将它们与 TypeScript 转换 API 一起使用,而不是执行 tsc 命令。有an issue 请求为自定义转换器提供插件支持。

      【讨论】:

      • 感谢您的回复,我昨天已经看到并安装了这个自定义转换器,但由于它使用的是 typescript 2.4,所以到目前为止这对我没有用。
      • 嗨,这个库也完全符合我的要求,但是,当我按照文档中的确切步骤操作时,我得到了ts_transformer_keys_1.keys is not a function。有解决办法吗?
      • 整洁!您认为它可以扩展为采用动态类型参数吗(自述文件中的注释 2)?
      • @HasithaShan 仔细查看文档 - 您必须使用 TypeScript 编译器 API 才能使包工作
      • 不幸的是,包坏了,无论我做什么我总是得到ts_transformer_keys_1.keys is not a function
      【解决方案7】:

      这应该可以工作

      var IMyTable: Array<keyof IMyTable> = ["id", "title", "createdAt", "isDeleted"];
      

      var IMyTable: (keyof IMyTable)[] = ["id", "title", "createdAt", "isDeleted"];
      

      【讨论】:

      • 并不是说它是错误的,但是在这里要清楚的是,您只是“强制数组的值”是正确的。开发人员仍然需要手动将它们写下来两次。
      • 虽然 Aidin 所说的可能是真的,但在某些情况下,这正是我所寻找的,就我而言。谢谢。
      • 这不会防止密钥重复或丢失密钥。喜欢var IMyTable: Array&lt;keyof IMyTable&gt; = ["id", "createdAt", "id"];
      • 对我来说,这也是我一直在寻找的,因为我想有选择地接受键,但无非是接口中定义的键。没想到这是上面代码的默认设置。我想我们仍然需要一种通用的 TS 方式。无论如何感谢上面的代码!
      【解决方案8】:
      // declarations.d.ts
      export interface IMyTable {
            id: number;
            title: string;
            createdAt: Date;
            isDeleted: boolean
      }
      declare var Tes: IMyTable;
      // call in annother page
      console.log(Tes.id);
      

      【讨论】:

      • 此代码将不起作用,因为 typescript 语法在运行时不可用。如果您在 typescript 操场上检查此代码,您会注意到编译为 JavaScript 的唯一内容是 console.log(Tes.id),这当然是错误“未捕获的 ReferenceError: Tes is not defined”
      【解决方案9】:

      以下要求您自己列出键,但至少 TypeScript 将强制 IUserProfileIUserProfileKeys 具有完全相同的键 (Required&lt;T&gt; was added in TypeScript 2.8):

      export interface IUserProfile  {
        id: string;
        name: string;
      };
      type KeysEnum<T> = { [P in keyof Required<T>]: true };
      const IUserProfileKeys: KeysEnum<IUserProfile> = {
        id: true,
        name: true,
      };
      

      【讨论】:

      • 很酷的把戏。现在很容易强制实现IUserProfile 的所有键,并且很容易从常量IUserProfileKeys 中提取它们。这正是我一直在寻找的。现在不需要将我所有的接口都转换为类。
      【解决方案10】:

      您需要创建一个实现接口的类,对其进行实例化,然后使用Object.keys(yourObject) 获取属性。

      export class YourClass implements IMyTable {
          ...
      }
      

      然后

      let yourObject:YourClass = new YourClass();
      Object.keys(yourObject).forEach((...) => { ... });
      

      【讨论】:

      • 在我的情况下不起作用,我必须列出接口的那些属性,但这不是我想要的?接口的名称是动态的,然后我必须确定它的属性
      • 这会产生错误(v2.8.3):Cannot extend an interface […]. Did you mean 'implements'? 但是,使用implements 需要手动复制界面,这正是我不想要的。
      • @jacob 抱歉,应该是implements,我已经更新了我的答案。正如@basarat 所说,接口在运行时不存在,因此唯一的方法是将其实现为类。
      • 您的意思是使用类而不是接口?不幸的是,我不能,因为界面来自第 3 方 (@types/react)。我手动复制了它们,但这几乎不是面向未来的? 我正在尝试动态绑定非生命周期方法(已经绑定),但它们没有在 React.Component (类)上声明。
      • 不,我的意思是创建一个实现您的 3rd 方接口的类并在运行时获取该类的属性。
      【解决方案11】:

      不要在接口中定义IMyTable,而是尝试将其定义为一个类。在打字稿中,您可以使用接口之类的类。

      因此,对于您的示例,像这样定义/生成您的类:

      export class IMyTable {
          constructor(
              public id = '',
              public title = '',
              public createdAt: Date = null,
              public isDeleted = false
          )
      }
      

      将其用作接口:

      export class SomeTable implements IMyTable {
          ...
      }
      

      获取密钥:

      const keys = Object.keys(new IMyTable());
      

      【讨论】:

        猜你喜欢
        • 1970-01-01
        • 2020-09-18
        • 2020-12-03
        • 2021-03-02
        • 2020-10-12
        • 2016-07-18
        • 2021-04-15
        • 2015-03-24
        • 1970-01-01
        相关资源
        最近更新 更多