安全变体
从具有安全编译时检查的接口创建键的数组或元组需要一点创造力。类型在运行时被擦除,并且对象类型(无序、命名)不能转换为元组类型(有序、无名),除非采用不受支持的技术。
与其他答案的比较
在给定引用对象类型(如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