Search code examples
typescript

Get property field names of type


I need to get all properties of type number of a type:

export interface ApplicationQuote {
    readonly chronoUnit: ApplicationQuote.ChronoUnitEnum;
    readonly downloadedDocs: number;
    readonly downloadedKb: number;
    readonly uploadedKb: number;
    readonly uploadedRefs: number;
}
export namespace ApplicationQuote {
    export type ChronoUnitEnum = 'HOUR' | 'DAY' | 'MONTH' | 'YEAR';
    export const ChronoUnitEnum = {
        HOUR: 'HOUR' as ChronoUnitEnum,
        DAY: 'DAY' as ChronoUnitEnum,
        MONTH: 'MONTH' as ChronoUnitEnum,
        YEAR: 'YEAR' as ChronoUnitEnum
    };
}

I need to get an Array<string> like ["downloadedDocs", "downloadedKb", "uploadedKb", "uploadedRefs"].

I've tried this code:

let names = Object.getOwnPropertyNames(ApplicationQuote);
let keys = Object.keys(ApplicationQuote);

enter image description here


Solution

  • I'm going to rename your ApplicationQuote interface to IApplicationQuote, to distinguish it from the ApplicationQuote namespace. One major issue you will immediately find is that you can't do anything like Object.keys(IApplicationQuote):

    Object.keys(IApplicationQuote); // error!
    // 'IApplicationQuote' only refers to a type, but is being used as a value here.
    

    An interface is a part of TypeScript's type system, which is completely erased from the emitted JavaScript. Interfaces only exist at design time, while you're writing the program, and can be checked at compile time, when the JavaScript is emitted... but they are gone by runtime. So there is nothing named IApplicationQuote at runtime to process.

    You will therefore need to write something like:

    const numericProps = [
      "downloadedDocs",
      "downloadedKb",
      "uploadedKb",
      "uploadedRefs"
    ] as const;
    

    (The as const is a const assertion which allows the compiler to treat numericProps as a tuple of string literals instead of as just a string[]).


    There are a few ways to proceed from here, then. One is to keep your interface and use the compiler to help ensure that your numericProps value has all and only the right members. First we can define KeysMatching<T, V>, which evaluates to a union of keys in T which are assignable to the type V:

    type KeysMatching<T, V> = NonNullable<
      { [K in keyof T]: T[K] extends V ? K : never }[keyof T]
    >;
    

    And use it like this:

    type NumericProps = KeysMatching<IApplicationQuote, number>;
    // type NumericProps = "downloadedDocs" | "downloadedKb" | "uploadedKb" | "uploadedRefs"
    

    Armed with the NumericProps type, we can write this:

    type MutuallyAssignable<T extends U, U extends V, V = T> = true;
    
    type NumericPropsOkay = MutuallyAssignable<
      typeof numericProps[number],
      NumericProps
    >; // okay
    

    You can use MutuallyAssignable<T, U> to ensure that the types you pass in for T and U are equivalent; if not, you will get an error (so MutuallyAssignable<string, string | "a"> is fine, but MutuallyAssignable<number, number | "a"> is not). Since NumericPropsOkay does not yield an error, you know you haven't made a mistake with numericProps.

    Consider what happens if you change numericProps like this:

    const numericProps = [
      "downloadedDocs",
      "downloadedKb",
      "uploadedkb", // <-- note the typo
      "uploadedRefs"
    ] as const;
    

    Then this will happen:

    type NumericPropsOkay = MutuallyAssignable<
      typeof numericProps[number], // error!
      NumericProps
    >;
    // Type '"uploadedkb"' is not assignable to type 
    // '"downloadedDocs" | "downloadedKb" | "uploadedKb" | "uploadedRefs"'.
    

    So you can use this to make sure your manually-written array and your interface definition don't diverge in the future.


    The other way to proceed is to define your interface in terms of the numericProps value. That is, do the reverse of what you asked for... JavaScript doesn't know anything about TypeScript, but TypeScript does know about JavaScript. So you can take this:

    const numericProps = [
      "downloadedDocs",
      "downloadedKb",
      "uploadedKb",
      "uploadedRefs"
    ] as const;
    

    And use it to define this interface:

    export interface IApplicationQuote
      extends Readonly<Record<typeof numericProps[number], number>> {
      readonly chronoUnit: ApplicationQuote.ChronoUnitEnum;
    }
    

    Here we have defined IApplicationQuote to extend Readonly<Record<typeof numericProps[number], number>>, which is essentially the same as {readonly downloadedDocs: number, readonly downloadedKb: number, ...}. We only had to add the one non-numeric property in there. And you can make sure it works:

    const appQuote: IApplicationQuote = {
      chronoUnit: "DAY",
      downloadedDocs: 1,
      downloadedKb: 2,
      uploadedKb: 3,
      uploadedRefs: 4
    }; // okay
    

    And you can verify that IApplicationQuote is still the type you're looking for:

    const badAppQuote: IApplicationQuote = {
      chronoUnit: "DAY",
      downloadedDocs: 1,
      downloadedKb: 2,
      uploadedkb: 3, // error!
      uploadedRefs: 4
    }; 
    // 'uploadedkb' does not exist in type 'IApplicationQuote'. 
    // Did you mean to write 'uploadedKb'?
    

    Looks good.


    Okay, hope that helps; good luck!

    Link to code