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

例子:

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

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

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

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

我如何实现这个结果?


当前回答

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

其他回答

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

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),如果有任何错误,其余的将总是引发一个错误。我认为这是最健壮的解决方案中简单,容易理解和易读的解决方案。

下面需要你自己列出键,但至少TypeScript会强制IUserProfile和IUserProfileKeys拥有完全相同的键(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,
};

在这个博客中

从数组中获取一个类型

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

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

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

安全的变体

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

与其他答案的比较

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

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

操场上

如果您不喜欢创建记录,可以看看这个具有Set和断言类型的替代方法。


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

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

+ tuple +-手动自动补全+-更高级,更复杂的类型

操场上

解释

createKeys通过将函数参数类型与附加断言类型合并来进行编译时检查,这些断言类型会在不合适的输入时发出错误。(keyof IMyTable)[] | [keyof IMyTable]是一种“黑魔法”方式,从被调用方强制推断一个元组而不是一个数组。或者,你可以从调用方使用const断言/作为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和“错误:缺少键”只是IDE错误。你也可以写成never。checkduplicate检查双元组项:

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

+ tuple +-手动自动补全+无辅助功能——性能


变体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

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

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


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

正如@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游乐场中的构造函数技巧。