typescripttype-inferencereact-typescript

Issue with Type Inference in TypeScript


Regarding TypeScript_Playground, I'm struggling with fixing type inference. I used a generic type to enforce type safety—specifically, if templateType is CIRCLE, I expect a type error when adding incompatible binding keys like RECTANGLE. However, that's not happening right now.

I suspect the issue might be with ShapeTemplate[]. When I change it to this way, I can no longer add multiple template types. Does anyone have expertise in type inference to help with this?


Solution

  • The issue is with your definition of the ShapeTemplate type. The current definition works well when the type generic is an "exact" type with no unions.

    So if you had a function like the following:

    const makeShape = <T extends ShapeTypes>(
      templateType: T,
      bindings: ShapeBindings[T],
    ) => {...};
    

    Then a call like the following:

    makeShape(ShapeTypes.CIRCLE, {
      [RectangleShapeBindingKey.RECTANGLE_PROPERTY_1]: true,
    });
    

    will return throw an error:

    Object literal may only specify known properties, and '[RectangleShapeBindingKey.RECTANGLE_PROPERTY_1]' does not exist in type 'Partial<{ CIRCLE_PROPERTY_1: string | undefined; }
    

    The problem you encounter arises with the following defintion of ShapeBase

    export interface ShapeBase {
      templates: ShapeTemplate<ShapeTypes>[];
      options?: any[];
    }
    

    If you expand the types, they look something like

    type T1 = {
        templateType: ShapeTypes;
        bindings?: Partial<{
            CIRCLE_PROPERTY_1: string | undefined;
        }> | Partial<{
            RECTANGLE_PROPERTY_1: boolean | undefined;
        }> | Partial<...> | undefined;
        info?: string | undefined;
    }
    

    You can see that there is no relationship between the types expected on templateType and bindings fields. This happens because once you pass the generic variable ShapeTypes to ShapeTemplate it simply gets passed into any place requiring it, without any particular logic binding fields to each other.

    Essentially, you are creating a type that says: I want templateType to satisfy ShapeTypes and bindings to satisfy ShapeBindings[T]; but in this case T is simply ShapeTypes so the bindings field will accept any value with a type that satisfies ShapeBindings[ShapeTypes]. And that is just going to be the union of all the values defined in the type.

    So basically what you want to do is change from something that abstractly looks like:

    type T<U> = {a:U1|U2|U3, b:F(U1)|F(U2)|F(U3)}
    

    to a type like

    type T<U> = {a:U1, b:F(U1)} | {a:U2, b:F(U2)} | {a:U3, b:F(U3)}
    

    So to achieve this, you want to create a mapped type that represents the union of all possible combinations and index it on its own keys, so something like this:

    type SomeShapeBase = {
      [K in keyof typeof ShapeTypes]: {
        templateType: (typeof ShapeTypes)[K];
        bindings: ShapeBindings[(typeof ShapeTypes)[K]];
      };
    }[keyof typeof ShapeTypes];