获取 Typescript 接口的键作为字符串数组

IT技术 javascript typescript
2021-01-16 19:05:49

我在Lovefield 中有很多表格以及它们各自的接口,用于显示它们具有的列。

例子:

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

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

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

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

我如何达到这个结果?

6个回答

TypeScript 2.3(或者我应该说2.4,因为在2.3 中,此功能包含已在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 命令。还有一个问题,请求定制变压器插件的支持。

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

也许为时已晚,但在 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

@CharlesCapps 这可以通过像这样包装 keyof 来轻松解决 type K2 = (keyof Person)[]
2021-03-22 19:05:49
我投了反对票,因为这不能解决上述问题。目标是获取string[]一个类型的列表不要使用keyof MyType.
2021-03-28 19:05:49
感谢您的回答,但不确定它是否有助于某人从接口使用静态创建的类型。恕我直言,我们可以在大多数情况下互换使用接口/类型。此外,这将需要为多个接口手动创建类型。但是,如果有人只需要从接口中获取类型,该解决方案看起来不错。
2021-03-30 19:05:49
对我很有用。我想为我的应用程序的本地化添加一些安全性(使用 expo-localisation)。你的回答帮助我实现了谢谢!(如果有人感兴趣,本文将逐步介绍)。
2021-04-10 19:05:49

以下要求您自己列出键,但至少 TypeScript 会强制执行IUserProfileIUserProfileKeys具有完全相同的键(Required<T>在 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并且很容易从 const 中提取它们IUserProfileKeys这正是我一直在寻找的。现在不需要将我所有的接口转换为类。
2021-03-17 19:05:49

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

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


这里要注意的一件事是,接口在编译时是强制类型,而对象主要是在运行时。来源

正如@derek 在另一个答案中提到的接口对象的共同点可以是一个同时提供类型值的类

所以,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,
};

这里是Typescript Playground上面的代码,可以多玩一些)

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

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

TypeScript Playground 中的构造函数技巧

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

安全变体

从具有安全编译时检查的接口创建的数组或元组需要一点创造力。类型在运行时被擦除,并且对象类型(无序、命名)不能转换为元组类型(有序、无名),除非采用不受支持的技术

与其他答案的比较

在给定引用对象类型(如IMyTable. 例如,声明一个数组类型(keyof IMyTable)[]无法捕获这些错误。

此外,它们不需要特定的库(最后一个变体使用ts-morph,我认为它是一个通用的编译器包装器),发出一个元组类型而不是一个对象(只有第一个解决方案创建一个数组)或宽数组类型(与这些 答案),最后不需要课程

变体 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")[]

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

操场

如果您不喜欢创建记录,请查看此替代方法 withSet和 assertion types


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

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

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

操场

解释

createKeys通过将函数参数类型与其他断言类型合并来进行编译时检查这些断言类型会针对不合适的输入发出错误。(keyof IMyTable)[] | [keyof IMyTable]是一种从被调用方强制推断元组而不是数组“黑魔法”方式或者,您可以从调用方使用const 断言/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 & "Error: missing keys"仅适用于不错的 IDE 错误。你也可以写never. CheckDuplicates检查双元组项目:

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

注意:关于元组中唯一项检查的更多信息在这篇文章中使用TS 4.1,我们还可以在错误字符串中命名缺失的键 - 看看这个 Playground


变体 3:递归类型

在 4.1 版中,TypeScript 正式支持条件递归类型,这也可以在这里使用。但是,由于组合复杂性,类型计算很昂贵 - 超过 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"] // ✔

++-具有自动完成+功能的元组手册无辅助功能--性能


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

此处选择了ts-morph,因为它是原始 TS 编译器 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 && node dist/mybuildstep.js./src/generated/IMyTable-keys.ts会生成一个包含以下内容的文件

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

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