Safe variants
Creating an array or tuple of keys from an interface with safety compile-time checks requires a bit of creativity. Types are erased at run-time and object types (unordered, named) cannot be converted to tuple types (ordered, unnamed) without resorting to non-supported techniques.
Comparison to other answers
The here proposed variants all consider/trigger a compile error in case of duplicate or missing tuple items given a reference object type like IMyTable
. For example declaring an array type of (keyof IMyTable)[]
cannot catch these errors.
In addition, they don't require a specific library (last variant uses ts-morph
, which I would consider a generic compiler wrapper), emit a tuple type as opposed to an object (only first solution creates an array) or wide array type (compare to these answers) and lastly don't need classes.
Variant 1: Simple typed array
// 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")[]
+
easiest +-
manual with auto-completion -
array, no tuple
Playground
If you don't like creating a record, take a look at this alternative with Set
and assertion types.
Variant 2: Tuple with helper function
function createKeys<T extends readonly (keyof IMyTable)[] | [keyof IMyTable]>(
t: T & CheckMissing<T, IMyTable> & CheckDuplicate<T>): T {
return t
}
+
tuple +-
manual with auto-completion +-
more advanced, complex types
Playground
Explanation
createKeys
does compile-time checks by merging the function parameter type with additional assertion types, that emit an error for not suitable input. (keyof IMyTable)[] | [keyof IMyTable]
is a "black magic" way to force inference of a tuple instead of an array from the callee side. Alternatively, you can use const assertions / as const
from caller side.
CheckMissing
checks, if T
misses keys from 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"]
Note: T & "Error: missing keys"
is just for nice IDE errors. You could also write never
. CheckDuplicates
checks double tuple items:
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"]]
Note: More infos on unique item checks in tuples are in this post. With TS 4.1, we also can name missing keys in the error string - take a look at this Playground.
Variant 3: Recursive type
With version 4.1, TypeScript officially supports conditional recursive types, which can be potentially used here as well. Though, the type computation is expensive due to combinatory complexity - performance degrades massively for more than 5-6 items. I list this alternative for completeness (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 +-
manual with auto-completion +
no helper function --
performance
Variant 4: Code generator / TS compiler API
ts-morph is chosen here, as it is a tad simpler wrapper alternative to the original TS compiler API. Of course, you can also use the compiler API directly. Let's look at the generator code:
// ./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
After we compile and run this file with tsc && node dist/mybuildstep.js
, a file ./src/generated/IMyTable-keys.ts
with following content is generated:
// ./src/generated/IMyTable-keys.ts
const keys = ["id","title","createdAt","isDeleted"] as const;
+
auto-generating solution +
scalable for multiple properties +
no helper function +
tuple -
extra build-step -
needs familiarity with compiler API