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

我在Lovefield中有很多表和它们各自的接口,用于它们有什么列。

例子:

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

我想有这个接口的属性名在这样的数组中:

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

我不能直接基于接口IMyTable创建对象/数组,因为我将动态地获得表的接口名称。因此,我需要在接口中迭代这些属性,并从中获得一个数组。

我如何实现这个结果?

355411 次浏览

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

一个解决方案:

创建一个类型为Object.keys的变量🌹

你需要创建一个类来实现你的接口,实例化它,然后使用Object.keys(yourObject)来获取属性。

export class YourClass implements IMyTable {
...
}

然后

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

对于打字稿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']

不幸的是,定制变压器目前还不太容易使用。你必须与打印稿转换API一起使用它们,而不是执行tsc命令。有一个问题请求自定义转换器的插件支持。

与其将IMyTable定义为interface,不如将其定义为类。在typescript中,你可以像接口一样使用类。

对于你的例子,像这样定义/生成你的类:

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());

下面要求你自己列出键,但至少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,
};
// 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);

这应该可以

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

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

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

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


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

正如@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中的上述代码,以播放更多)

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

TypeScript游乐场中的构造函数技巧

安全的变体

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

与其他答案的比较

在给定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")[]

+最简单的+-手动带自动补全的-数组,没有元组

< a href = " https://www.typescriptlang.org/play?代码/ JYOwLgpgTgZghgYwgAgJIFkCeAVOAjAGxQG8AoZZYAEwC5kQBXAWz2gG5zkxgwi6BnMFFABzDhQRQIcSFQCCYOgBEZEcZX5KIRWXTwB7fUTggOAX1KkA9FeQAlCAn1QqXTAAcUEEPwZT % 20 adtiao4oabzwag4oiprivpomhcjoyeza-pyiyadwejibyjfwbawq-mgijsis9bairaiypdamie3a % 20 icvuqoa0vn8abr5ma5olntjzlqapkp6mghyumlbjpgafacudcp5i8s4 % 20 etbanoaushkffjgft0a8ngavo5gahsjw6ptltvioavdakcykjw % 20 mc5qbiac8pwkkagbsgxa0wh0efoyaajefjiisqo6hiudw % 20 ligtqscgzntrlyiyjoqu9gaiahs5aah2qbo4vagxn5bijqnkygffiygigsjz5wupcaa”游乐场rel = " noreferrer " > < / >

如果你不喜欢创建记录,看一下这个选项带有Set和断言类型


变体2:带有helper函数的元组

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

+元组+-手动自动补全+-更高级,复杂类型

<一个href = " https://www.typescriptlang.org/play?代码/ JYOwLgpgTgZghgYwgAgJIFkCeAVOAjAGxQG8AoZC5YAEwC5kQBXAWz2gG5zKxgwj6AzmCigA5p0rIEUCHEjUAgmHoARORAmVgAlRCLz6eAPZGicEJwC + pUgHpbyAMIALCAgDWAgDRUYybMjM2gIQAsjuEJhhMFBGzMgAqhSkYJgADigubu7owWIAPAEQAB6QINRhMnDURiAEmMjmmADaALo + SSVlFcgASm5GUNT5QiIgoj5NAHxTyAC8yGSSzQDSVCDhkUZ + Ca30a10Q5WHYzUys0K3IAPwMEABu0Mj7pJbNEZjbiVeHx3ePUBu-mewIAZMgAEQAUSgsSg9CCAgEYk2UQhdgcADkjJB6AADALg6GwwYIvLjVECCF4qhhABWjCEyBggwYwCQaBUUOQ0DhAgAdMgAJpGRhSUUEaiNAgCIzIADuIkgyDxIAe0Dx-Js9icrg83 l8wiqttacfaywojdsbhz6iokgyahs6uyevckitnoq6kkpnkrx6vrqdqattaswws0ozqacgbgdaor7ypaqgd6maicfeayhfu6tskxgqsyatimpsnuzhy78evggwqac8ujikkcaz9-x8o3grrcirmszm8-qm083pw-cmbq2s732pcyxd6jbrbbibcrvqf68bdbgcaedxaliqpavpebl665vzmh6sgabrttbyxcecaasjayaapiwr4ydg + BEK0UxPnOyhgrq2S5EiBTYD4wEfkQszglkHgeuu3qQIUUxfjuixzjIYCMFAGxgHupDGiATJGO4-QCIwBBgPMZ6yBeV5Ps0EI0BCPgQjwfAQAJkLSJxECKGAYl8ToegQPIW5fsgOoAPIrDR54QJeUQ8Xx1DKapDi8oMPiIsiFK8BAjpaZJukCPpACOjDFEZOqmVAPggLUAC0zByAgzgotZzB2eoDn6fxgnCUQskSeo0mydouj6FJyWGa0KkeSSXnIGuXp2qFNi0bKRD8gQRiiE + DFMSxYBftqDiQEI0Ssq4BAZICqQZE6PUoNgCYLJhOTkqI + S8WkcZbj4xZTfQTQ + GkpYLSADSWLMOqTdNVxEsupKBGNlLov1-jlsNbrwZZ43bbJy0zYsyDzY0a1LStL3rZtDi3YJ92tM6GT + AAzOxI3YYVeHNHGPilj4QPgQDA0ACyg264MbhAE3Q8gsPIH2UxAA noreferrer“rel = >操场< / >

解释

createKeys在编译时检查带有附加断言类型的通过合并函数形参类型,这些断言类型会在不合适的输入时发出错误。(keyof IMyTable)[] | [keyof IMyTable]是一个“黑magic"道路,用于强制从被调用方推断元组而不是数组。或者,你可以从调用方使用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错误。你也可以写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"]]

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


变体3:递归类型

在4.1版中,TypeScript正式支持条件递归类型,它也可以在这里使用。然而,由于组合的复杂性,类型计算是昂贵的——超过5-6个项目的性能会大幅下降。为了完整性,我列出了这个替代选项(操场上):

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

也许太晚了,但是在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

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

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

选项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)

如果您不能使用自定义转换器(或不愿意使用),我认为最好的方法就是我将要展示的方法,它具有以下优点:

  1. 它允许“半自动”;填充数组(至少在VS Code中);
  2. 它会生成一个数组,TypeScript会将其识别为包含接口键的并集元素;
  3. 它不涉及降低性能的递归技巧。

方法如下:

interface Foo {
fizz?: string;
buzz: number;
}


const FooKeysEnum: { [K in keyof Required<Foo>]: K } = {
fizz: 'fizz',
buzz: 'buzz',
};


const FooKeys = Object.values(FooKeysEnum);

“semi-automatic"在VS Code中填充数组的原因是,当FooKeysEnum因为缺少属性而有红色下划线时,你可以将鼠标悬停在它上面并选择“添加缺少属性”。从“快速修复”;菜单。(这个好处已经在这篇文章中展示过了,但我认为还没有人提到过。埃塔:我弄错了;自动补全已经在线程的其他地方提到了。)

最后,通过使用Object.values()而不是Object.keys()来创建数组,你可以让TypeScript识别出FooKeys的类型是("fizz" | "buzz")[]。它不知道FooKeys[0]"fizz"FooKeys[1]"buzz",但仍然比你用Object.keys()得到的string[]要好。

编辑:

在VS Code中,你也可以在keybindings.json中为执行设置键盘快捷键“Quick Fix”;这使得触发Add missing properties更快。看起来是这样的:

{
"key": "shift+cmd+a",
"command": "editor.action.codeAction",
"args": {
"kind": "quickfix",
"apply": "ifSingle"
}
}

然后,如果某个东西有红色下划线,你可以单击它并使用键盘快捷键,如果只有一个可用的快速修复选项,那么它将运行。如果有一种方法可以针对具体的进行快速修复,那就太好了,如果它可以在文件保存时自动完成,那就更好了,但我认为在撰写本文时这是不可能的。

有些人建议这样做,这是最简单的解决方案:

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

然而,尽管这增加了一些类型安全性(我们不能错误地使用不存在的属性),但这并不是一个完全安全的解决方案,因为我们可能会错过一些属性并拥有重复的属性。所以我已经修复了这个问题,这个详细的解决方案是完全类型安全的,并防止了数组的编译时类型和运行时值之间的不一致:

const properties: [
keyof Pick<IMyTable, 'id'>,
keyof Pick<IMyTable, 'title'>,
keyof Pick<IMyTable, 'createdAt'>,
keyof Pick<IMyTable, 'isDeleted'>
] = ['id', 'title', 'createdAt', 'isDeleted'];

当然,这只适用于如果你不避免重复,但至少你只需要确保你正确地写所有属性一次(在Pick类型util),如果有任何错误,其余的将总是引发一个错误。我认为这是最健壮的解决方案中简单,容易理解和易读的解决方案。

正如其他人已经说过的,这些类型在运行时不可用,因此您需要生成它们或手工编写它们。

如果你手写它们,那么简单的Array<keyof IMyTable>不会验证缺失的键。

这是一个非常好的答案,它回答了用类型安全声明键数组的问题。(功劳归于他。)相关代码:

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


type Invalid<T> = ['Needs to be all of', T];
const arrayOfAll =
<T>() =>
<U extends T[]>(
...array: U & ([T] extends [U[number]] ? unknown : Invalid<T>[])
) =>
array;


const fields = arrayOfAll<keyof IMyTable>()(
'id',
'createdAt',
'title',
'isDeleted',
);

如果缺少字段,则会显示错误。

问题是类型在运行时不可用。我最终定义了一个对象,并从该对象派生了类型。您仍然可以获得类型支持,并获得从单个位置获得所有键的列表的能力。

const myTable =  {
id: 0,
title: '',
createdAt: null as Date,
isDeleted: false,
};


export type TableType = typeof myTable;
export type TableTypeKeys = (keyof TableType)[];
export const tableKeys: TableTypeKeys = Object.keys(
myTable
) as TableTypeKeys;

由上面产生的隐式类型将是(你可以用VSCode或其他高质量IDE检查):

type TableType = {
id: number;
title: string;
createdAt: Date;
isDeleted: boolean;
}


type TableTypeKeys = ("id" | "title" | "createdAt" | "isDeleted")[]


在运行时,你将能够访问tableKeys数组

console.log(tableKeys);
// output: ["id" , "title" , "createdAt" , "isDeleted"]

在这个博客

从数组中获取一个类型

现在我们可以使用typeof从animals数组中获取一个类型:

const animals = ['cat', 'dog', 'mouse'] as const
type Animal = typeof animals[number]


// type Animal = 'cat' | 'dog' | 'mouse'