Search code examples
typescripttypescript-typingsmapped-types

Create an interface/type alias with an exhaustive list of properties that comes from a union type?


Imagine I have this array, which is the "source of truth" for my action names:

const ACTION_NAMES = ["ACTION_1","ACTION_1","ACTION_3"] as const;

// I'M USING "as const" SO I CAN GET A UNION TYPE OUT OF IT

type ACTION_NAMES = typeof ACTION_NAMES[number];

// THIS IS "ACTION_1" | "ACTION_2" | "ACTION_3"

Now I want an interface/type which should have all these properties (exhaustive), as in:

type SOME_TYPE = {
  [key: string]: unknown
}

interface ACTION_PAYLOADS {  // COULD ALSO USE type alias HERE
  ACTION_1: {propA: string, propB: number},
  ACTION_2: {propC: boolean},
  ACTION_3: {propD: string}
}

Is there any type I can extend the ACTION_PAYLOADS interface from, so that Typescript will make sure that all ACTION_1 2 and 3 properties will be present, and all of their values will extend the SOME_TYPE type?

The idea is that Typescript ideally should complain if I'm missing one of the ACTION_ properties, and also if one of the values isn't of type SOME_TYPE. Also it wouldn't allow me to add any extra properties, for example ACTION_4.

It feels like I'm type checking a type. But can't I do it with some mapped type? Is there a way to achieve this?


Solution

  • You could use some helper types to trigger a compiler error if your interface doesn't conform to the desired constraints. It looks like this:

    type Extends<T extends U, U> = void;
    type MutuallyExtends<T extends U, U extends V, V = T> = void;
    
    type CheckKeysOfActionPayloads =
        MutuallyExtends<keyof ACTION_PAYLOADS, ACTION_NAMES>; // ok
    type CheckValuesOfActionPayloads =
        Extends<ACTION_PAYLOADS[ACTION_NAMES], SOME_TYPE>; // ok
    

    Here, CheckKeysOfActionPayloads will only compile if the keys of ACTION_PAYLOADS and the type ACTION_NAMES mutually extend each other... for a union of literals this means they need to be identical.

    And CheckValuesOfActionPayload will only compiler if the values of ACTION_PAYLOADS (at the ACTION_NAMES keys) are assignable to SOME_TYPE.


    For concreteness I'm going to change SOME_TYPE to be something that can fail, and also let's make sure that ACTION_NAMES has "ACTION_2" in it (as opposed to your example code):

    const ACTION_NAMES = ["ACTION_1", "ACTION_2", "ACTION_3"] as const;
    type ACTION_NAMES = typeof ACTION_NAMES[number];
    type SOME_TYPE = {
        [key: string]: string | number | boolean
    }
    

    Let's see what happens if we mess up ACTION_PAYLOADS. First, if we miss a key:

    interface ACTION_PAYLOADS {
        ACTION_1: { propA: string, propB: number },
        // where's ACTION_2?
        ACTION_3: { propD: string }
    }
    
    type CheckKeysOfActionPayloads =
        MutuallyExtends<keyof ACTION_PAYLOADS, ACTION_NAMES>; // error!
    //      ----------------------------------> ~~~~~~~~~~~~
    // Type "ACTION_2" is not assignable to type "ACTION_1" | "ACTION_3"
    

    Or if we have an extra key:

    interface ACTION_PAYLOADS {
        ACTION_1: { propA: string, propB: number },
        ACTION_2: { propC: boolean },
        ACTION_3: { propD: string },
        ACTION_4: { propE: number } // what's this?
    }
    
    type CheckKeysOfActionPayloads =
        MutuallyExtends<keyof ACTION_PAYLOADS, ACTION_NAMES>; // error!
    // ---------------> ~~~~~~~~~~~~~~~~~~~~~
    // Type "ACTION_4" is not assignable to type "ACTION_1" | "ACTION_2" | "ACTION_3"
    

    And finally if the keys are right but a value is wrong:

    interface ACTION_PAYLOADS {
        ACTION_1: { propA: string, propB: number },
        ACTION_2: { propC: Date }, // what's this?
        ACTION_3: { propD: string }
    }
    
    type CheckValuesOfActionPayloads =
        Extends<ACTION_PAYLOADS[ACTION_NAMES], SOME_TYPE>; // error!
    // -------> ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
    // Type 'Date' is not assignable to type 'string | number | boolean'
    

    You can see that in all those cases you get an error that can hopefully be used to help you fix the code.

    Playground link to code