我在 Lovefield 中有很多表以及它们各自的接口,它们有哪些列。
例子:
export interface IMyTable {
id: number;
title: string;
createdAt: Date;
isDeleted: boolean;
}
我想将此接口的属性名称放在这样的数组中:
const IMyTable = ["id", "title", "createdAt", "isDeleted"];
我不能直接基于接口 IMyTable
创建对象/数组,这应该可以解决问题,因为我将动态获取表的接口名称。因此,我需要在接口中迭代这些属性并从中获取一个数组。
我怎样才能达到这个结果?
从 TypeScript 2.3 开始(或者我应该说 2.4,就像在 2.3 中一样,此功能包含已在 typescript 中修复的 a bug @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.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
string[]
的列表;不要使用 keyof MyType
获取密钥类型。
type K2 = (keyof Person)[]
仍然是一种类型,而不是数组。
以下要求您自己列出键,但至少 TypeScript 将强制 IUserProfile
和 IUserProfileKeys
具有完全相同的键 (Required<T>
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
的所有键,并且很容易从 const IUserProfileKeys
中提取它们。这正是我一直在寻找的。现在不需要将我所有的接口都转换为类。
我遇到了类似的问题:我有一个巨大的属性列表,我想将它们作为接口(编译时)和对象(运行时)。
注意:我不想写(用键盘输入)属性两次!干燥。
这里要注意的一件事是,接口在编译时是强制类型,而对象主要是运行时。 (Source)
正如@derek 在 another answer 中提到的,interface 和 object 的共同点可以是一个同时服务于 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
初始化它们是最好的方法。
typeof
运算符进行运行时类型检查。
安全变体
从具有安全编译时检查的接口创建一个数组或 tuple 键需要一点创造力。类型在运行时被擦除,对象类型(无序、命名)不能转换为元组类型(有序、无名)without resorting to non-supported techniques。
与其他答案的比较
在给定参考对象类型(如 IMyTable
)的情况下,此处提出的变体都考虑/触发编译错误,以防出现重复或缺失的元组项。例如,声明 (keyof IMyTable)[]
的数组类型无法捕获这些错误。
此外,它们不需要 a specific library(最后一个变体使用 ts-morph
,我认为它是一个通用编译器包装器),发出元组类型 as opposed to an object(只有第一个解决方案创建一个数组)或宽数组类型(比较到 these answers),最后是 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")[]
+
最简单的 +-
自动完成手册 -
数组,没有元组
如果您不喜欢创建记录,请查看 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
}
+
元组 +-
具有自动完成功能的手册 +-
更高级、更复杂的类型
解释
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 & "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"]]
注意:有关元组中唯一项检查的更多信息在 this post 中。使用 TS 4.1,我们还可以命名错误字符串中缺少的键 - 看看 this Playground。
变体 3:递归类型
在 4.1 版本中,TypeScript 正式支持 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"] // ✔
+
元组 +-
具有自动完成功能的手册 +
无辅助函数 --
性能
变体 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 && node dist/mybuildstep.js
编译并运行此文件后,会生成一个包含以下内容的文件 ./src/generated/IMyTable-keys.ts
:
// ./src/generated/IMyTable-keys.ts
const keys = ["id","title","createdAt","isDeleted"] as const;
+
自动生成解决方案 +
可针对多个属性进行扩展 +
无辅助函数 +
元组 -
额外的构建步骤 -
需要熟悉编译器 API
这应该工作
var IMyTable: Array<keyof IMyTable> = ["id", "title", "createdAt", "isDeleted"];
或者
var IMyTable: (keyof IMyTable)[] = ["id", "title", "createdAt", "isDeleted"];
var IMyTable: Array<keyof IMyTable> = ["id", "createdAt", "id"];
不要将 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());
您需要创建一个实现接口的类,对其进行实例化,然后使用 Object.keys(yourObject)
获取属性。
export class YourClass implements IMyTable {
...
}
然后
let yourObject:YourClass = new YourClass();
Object.keys(yourObject).forEach((...) => { ... });
Cannot extend an interface […]. Did you mean 'implements'?
但是,使用 implements
需要手动复制界面,这正是我不想要的。
implements
,我已经更新了我的答案。正如@basarat 所说,接口在运行时不存在,所以唯一的方法是将它作为一个类来实现。
@types/react
)。我手动复制了它们,但这几乎不是面向未来的😪 我正在尝试动态绑定非生命周期方法(已经绑定),但它们没有在 React.Component (类)上声明。
这是一个艰难的!谢谢大家,谢谢你的帮助。
我的需要是将接口的键作为字符串数组来简化 mocha/chai 脚本。不关心在应用程序中使用(还),所以不需要创建 ts 文件。感谢 ford04 的帮助,他的解决方案 above 提供了巨大的帮助,并且运行良好,没有编译器黑客攻击。这是修改后的代码:
方案二:基于 TS 编译器 API(ts-morph)的代码生成器
节点模块
npm install --save-dev ts-morph
键.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)
有人提出了这个建议,它的好处是最简单的解决方案:
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 类型实用程序上),其余的总是会引发错误,如果有任何错误。我认为它是简单、易于理解和可读的解决方案中最强大的解决方案。
如果您不能使用自定义转换器(或者不想使用),我认为最好的方法就是我将要展示的内容,它具有以下优点:
它允许数组的“半自动”填充(至少在 VS Code 中);它会生成一个数组,TypeScript 将其识别为具有接口键联合的元素;它不涉及降低性能的递归技巧。
这是方法:
interface Foo {
fizz?: string;
buzz: number;
}
const FooKeysEnum: { [K in keyof Required<Foo>]: K } = {
fizz: 'fizz',
buzz: 'buzz',
};
const FooKeys = Object.values(FooKeysEnum);
VS Code 中数组的“半自动”填充来自这样一个事实:当 FooKeysEnum
因为缺少属性而带有红色下划线时,您可以将鼠标悬停在它上面并从“快速修复”菜单中选择“添加缺少的属性” . (这个好处已经在这个线程中显示的其他方法共享,但我认为还没有人提到它。ETA:我在这里弄错了;在线程的其他地方已经提到了自动完成。)
最后,通过使用 Object.values()
而不是 Object.keys()
创建数组,您可以让 TypeScript 识别出 FooKeys
的类型为 ("fizz" | "buzz")[]
。它不知道 FooKeys[0]
是 "fizz"
并且 FooKeys[1]
是 "buzz"
,但仍然比使用 Object.keys()
获得的 string[]
更好。
编辑:
在 VS Code 中,您还可以在 keybindings.json
中设置键盘快捷键以执行“快速修复”,从而更快地触发 Add missing properties
。看起来像这样:
{
"key": "shift+cmd+a",
"command": "editor.action.codeAction",
"args": {
"kind": "quickfix",
"apply": "ifSingle"
}
}
然后,如果某些东西有红色下划线,您可以单击它并使用键盘快捷键,如果只有一个快速修复选项可用,那么它将运行。如果有一种方法可以针对特定的快速修复,那就太好了,如果可以在文件保存时自动完成,那就更好了,但我认为在撰写本文时这是不可能的。
不能。接口在运行时不存在。
一种解决方法:
创建一个类型的变量并在其上使用 Object.keys
🌹
var abc: IMyTable = {}; Object.keys(abc).forEach((key) => {console.log(key)});
// 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);
console.log(Tes.id)
,这当然是错误“未捕获的 ReferenceError:未定义 Tes”
ts_transformer_keys_1.keys is not a function
。有解决方法吗?ts_transformer_keys_1.keys is not a function
ts_transformer_keys_1.keys is not a function
的 Next.js 破坏了这个包。如果这个问题得到解决,将会投票。