Get keys of a Typescript interface as array of strings

后端 未结 12 1702
攒了一身酷
攒了一身酷 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:34

    As of TypeScript 2.3 (or should I say 2.4, as in 2.3 this feature contains a bug which has been fixed in typescript@2.4-dev), you can create a custom transformer to achieve what you want to do.

    Actually, I have already created such a custom transformer, which enables the following.

    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']
    

    Unfortunately, custom transformers are currently not so easy to use. You have to use them with the TypeScript transformation API instead of executing tsc command. There is an issue requesting a plugin support for custom transformers.

    0 讨论(0)
  • 2020-11-28 03:35

    Instead of defining IMyTable as in interface, try defining it as a class. In typescript you can use a class like an interface.

    So for your example, define/generate your class like this:

    export class IMyTable {
        constructor(
            public id = '',
            public title = '',
            public createdAt: Date = null,
            public isDeleted = false
        )
    }
    

    Use it as an interface:

    export class SomeTable implements IMyTable {
        ...
    }
    

    Get keys:

    const keys = Object.keys(new IMyTable());
    
    0 讨论(0)
  • 2020-11-28 03:40

    I had a similar problem that I had a giant list of properties that I wanted to have both an interface, and an object out of it.

    NOTE: I didn't want to write (type with keyboard) the properties twice! Just DRY.


    One thing to note here is, interfaces are enforced types at compile-time, while objects are mostly run-time. (Source)

    As @derek mentioned in another answer, the common denominator of interface and object can be a class that serves both a type and a value.

    So, TL;DR, the following piece of code should satisfy the needs:

    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 is the above code in Typescript Playground to play more)

    PS. If you don't want to assign initial values to the properties in the class, and stay with the type, you can do the constructor trick:

    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.

    0 讨论(0)
  • 2020-11-28 03:40

    This was a tough one! Thank you, everyone, for your assistance.

    My need was to get keys of an interface as an array of strings to simplify mocha/chai scripting. Not concerned about using in the app (yet), so didn't need the ts files to be created. Thanks to ford04 for the assistance, his solution above was a huge help and it works perfectly, NO compiler hacks. Here's the modified code:

    Option 2: Code generator based on TS compiler API (ts-morph)

    Node Module

    npm install --save-dev ts-morph
    

    keys.ts

    NOTE: this assumes all ts files are located in the root of ./src and there are no subfolders, adjust accordingly

    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;
    
    

    usage

    import keys from "./keys";
    
    const myKeys = keys("MyInterface") //ts file name without extension
    
    console.log(myKeys)
    
    0 讨论(0)
  • 2020-11-28 03:42

    The following requires you to list the keys on your own, but at least TypeScript will enforce IUserProfile and IUserProfileKeys have the exact same keys (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,
    };
    
    0 讨论(0)
  • 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, 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

    0 讨论(0)
提交回复
热议问题