Skip to content

Distributive Conditional Types in Mapped Types #1519

@huanglei1991

Description

@huanglei1991

Description

Distributive Conditional Types in Mapped Types cause incorrect type evaluation for union UserField

When defining a mapped type (like Fields) with a conditional check on a generic type parameter (like UserField), if UserField is a union type, TypeScript's distributive behavior causes the entire mapped type to become a union of objects (Object<A> | Object<B>) instead of a single object where each property can be any type from the union. This prevents mixing different custom fields within the same configuration object.

Environment

  • Puck version: [Your Version, e.g., 0.21.0]
  • TypeScript version: [e.g., 5.0+]
  • Additional environment info: Occurs in any environment using TypeScript for component configuration.

Steps to reproduce

  1. Define a Fields type with a naked conditional check on UserField:
type ReactElement = any;

export type DefaultComponentProps = { [key: string]: any };

export type Metadata = { [key: string]: any };
export interface FieldMetadata extends Metadata {}

export interface BaseField {
  label?: string;
  labelIcon?: ReactElement;
  metadata?: FieldMetadata;
  visible?: boolean;
}

export interface TextField extends BaseField {
  type: "text";
  placeholder?: string;
  contentEditable?: boolean;
}

export interface NumberField extends BaseField {
  type: "number";
  placeholder?: string;
  min?: number;
  max?: number;
  step?: number;
}

export type Field<ValueType = any, UserField extends {} = {}> =
  | TextField
  | NumberField
  | ObjectField<ValueType, UserField>


export type Fields<
  ComponentProps extends DefaultComponentProps = DefaultComponentProps,
  UserField extends {} = {}
> = {
  [PropName in keyof Omit<ComponentProps, "editMode">]: UserField extends {
    type: PropertyKey;
  }
    ? Field<ComponentProps[PropName], UserField> | UserField
    : Field<ComponentProps[PropName]>;
};


export interface ObjectField<
  Props extends any = { [key: string]: any },
  UserField extends {} = {}
> extends BaseField {
  type: "object";
  objectFields: {
    [SubPropName in keyof Props]: UserField extends { type: PropertyKey }
      ? Field<Props[SubPropName]> | UserField
      : Field<Props[SubPropName]>;
  };
}



interface Props {
  name: string;
  number: number;
  ob: {
    user: string;
    color: string;
    name: string;
  };
}

interface ColorField extends BaseField {
  type: "color";
}
interface UserField extends BaseField {
  type: "user";
}

type MyUserField = ColorField | UserField;

const fields: Fields<Props, MyUserField> = {
  name: {
    type: "user",
  },
  number: {
    type: "color",
  },
  ob: {
    type: "object",
    // error
    objectFields: {
      name: {
        type: "text",
      },
      user: {
        type: "user",
      },
      color: {
        type: "color",
      },
    },
  },
};

What happens

TypeScript throws an error because it expects the object to either be Fields<ColorField> or Fields<CustomField>, but not a mixture of both.
Error: Type '{ prop1: ...; prop2: ...; }' is not assignable to type 'Fields<ColorField> | Fields<CustomField>'.

What I expect to happen

The Fields type should resolve to a single object where each property can independently be any valid field defined in the UserField union.

Suggested Solution

Wrap the generic type parameter in square brackets [] to disable distribution:

export type Fields<
  ComponentProps extends DefaultComponentProps = DefaultComponentProps,
  UserField extends {} = {}
> = {
  [PropName in keyof Omit<ComponentProps, "editMode">]: [UserField] extends [{
    type: PropertyKey;
  }]
    ? Field<ComponentProps[PropName], UserField> | UserField
    : Field<ComponentProps[PropName]>;
};
export interface ObjectField<
  Props extends any = { [key: string]: any },
  UserField extends {} = {}
> extends BaseField {
  type: "object";
  objectFields: {
    [SubPropName in keyof Props]: [UserField] extends [{ type: PropertyKey }]
      ? Field<Props[SubPropName]> | UserField
      : Field<Props[SubPropName]>;
  };
}

Metadata

Metadata

Assignees

No one assigned

    Labels

    Type

    No type

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions