-
Notifications
You must be signed in to change notification settings - Fork 836
Description
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
- Define a
Fieldstype with a naked conditional check onUserField:
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]>;
};
}