Get keys of a Typescript interface as array of strings

后端 未结 12 1730
攒了一身酷
攒了一身酷 2020-11-28 03:05

I\'ve a lot of tables in Lovefield and their respective Interfaces for what columns they have.
Example:

export interface IMyTable {
  id: number;
  t         


        
12条回答
  •  粉色の甜心
    2020-11-28 03:43

    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)[] {
      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: T & CheckMissing & CheckDuplicate): 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> = {
        [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 = {
        [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] // TS 4.0 variadic tuples
    
    type Keys> = Keys_
    type Keys_, U extends PropertyKey[]> =
      {
        [P in keyof T]: {} extends Omit ? [P] : Prepend, U>>
      }[keyof T]
    
    const t1: Keys = ["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

提交回复
热议问题