Prerequisites
What theme are you using?
core
Is your feature request related to a problem? Please describe.
Currently, UiSchema is loosely typed. Any widget can be assigned to any field, and there's no autocomplete or validation for type-specific options like minValue (numbers) or placeholder (strings).
// Current behavior - no type errors, no autocomplete
const uiSchema: UiSchema = {
name: { 'ui:widget': 'RangeWidget' }, // Wrong! RangeWidget is for numbers
age: { 'ui:placeholder': 'Enter age' }, // Wrong! placeholder is for strings
};
Describe the solution you'd like
Core Pattern: { when, then }
TS Playground to see implementation
Define type-specific options using a simple pattern:
type BaseChecks =
| { when: unknown[]; then: { widget?: 'CheckboxesWidget'; addable?: boolean; orderable?: boolean; removable?: boolean } }
| { when: number; then: { widget?: 'TextWidget' | 'RangeWidget' | 'UpDownWidget'; minValue?: number; maxValue?: number; step?: number } }
| { when: string; then: { widget?: 'TextWidget' | 'TextareaWidget' | 'PasswordWidget'; placeholder?: string } }
| { when: boolean; then: { widget?: 'CheckboxWidget' | 'RadioWidget' } }
| { when: object & { length?: never }; then: { widget?: 'ObjectField'; collapsible?: boolean; defaultExpanded?: boolean } };
Main Type
// T defaults to any for vanilla usage without type param
type UiOptions<T = any, Checks = never> =
CommonOpts &
ComponentOpts<T, BaseChecks | Checks> &
WithUiPrefix<RawOptsFor<T, BaseChecks | Checks>> &
(T extends (infer U)[] ? { items?: UiOptions<U, Checks> } : {}) &
(T extends unknown[] ? {} : T extends object ? {
'ui:order'?: (keyof T | '*')[];
} & { [K in keyof T]?: UiOptions<T[K], Checks> } : {});
Theme Extension
Themes can add custom widgets scoped to specific types:
// Theme adds ToggleWidget for booleans only
type MyThemeChecks =
| { when: boolean; then: { widget?: 'ToggleWidget' } }
| { when: string[]; then: { widget?: 'CommaSeparatedWidget' } }
| { when: number; then: { widget?: 'SliderWidget' } };
type MyThemeUiOptions<T = any, Checks = never> = UiOptions<T, MyThemeChecks | Checks>;
User Extension
Users can add domain-specific options:
type MyChecks =
| { when: { isUser: true }; then: { admin?: boolean; widget?: 'UserWidget' } }
| { when: { isProduct: true }; then: { showPrice?: boolean } };
type MyUiOptions<T = any, Checks = never> = MyThemeUiOptions<T, MyChecks | Checks>;
Usage Examples
interface FormData {
age: number;
active: boolean;
tags: string[];
user: { name: string; isUser: true };
}
const uiSchema: MyUiOptions<FormData> = {
// Type-safe field ordering
'ui:order': ['active', 'age', 'tags', 'user', '*'],
// number gets: RangeWidget, SliderWidget, minValue, maxValue, step
age: { 'ui:widget': 'SliderWidget', 'ui:minValue': 0 },
// boolean gets: CheckboxWidget, RadioWidget, ToggleWidget (from theme)
active: { 'ui:widget': 'ToggleWidget' },
// string[] gets: CheckboxesWidget, CommaSeparatedWidget (from theme), addable, etc.
tags: { 'ui:widget': 'CommaSeparatedWidget' },
// Objects matching { isUser: true } get admin option
user: {
'ui:admin': true,
name: { 'ui:placeholder': 'Enter name' },
},
};
// Type errors caught at compile time:
const bad: MyUiOptions<{ name: string }> = {
name: { 'ui:widget': 'RangeWidget' }, // Error: RangeWidget not valid for string
};
Key Design Decisions
Widgets use Union, Options use Intersection
When multiple checks match (e.g., number matches both base and theme checks):
- Widgets: Combined as union (all valid widgets available)
- Options: Combined as intersection (all options merged)
// number matches BaseChecks AND ThemeChecks
// Widget options: 'TextWidget' | 'RangeWidget' | 'UpDownWidget' | 'SliderWidget'
// Options merged: { minValue?: number; maxValue?: number; step?: number }
Both Formats Supported
// ui:widget format
{ 'ui:widget': 'ToggleWidget' }
// ui:options format
{ 'ui:options': { widget: 'ToggleWidget' } }
// ui:prefixed options
{ 'ui:minValue': 0, 'ui:maxValue': 100 }
// Inside ui:options
{ 'ui:options': { minValue: 0, maxValue: 100 } }
Extensibility Chain
Each layer can pass through the Checks parameter:
UiOptions<T, Checks>
↓ adds ThemeChecks
ThemeUiOptions<T, Checks>
↓ adds UserChecks
UserUiOptions<T, Checks>
↓ further extensible...
Implementation
Full implementation (~60 lines)
// Type-specific checks using { when, then } pattern
type BaseChecks =
| { when: unknown[]; then: { widget?: 'CheckboxesWidget'; field?: 'ArrayField'; addable?: boolean; orderable?: boolean; removable?: boolean } }
| { when: number; then: { widget?: 'TextWidget' | 'RangeWidget' | 'UpDownWidget'; minValue?: number; maxValue?: number; step?: number } }
| { when: string; then: { widget?: 'TextWidget' | 'TextareaWidget' | 'PasswordWidget'; field?: 'StringField'; placeholder?: string } }
| { when: boolean; then: { widget?: 'CheckboxWidget' | 'RadioWidget'; field?: 'BooleanField' } }
| { when: object & { length?: never }; then: { widget?: 'ObjectField'; field?: 'ObjectField'; collapsible?: boolean; defaultExpanded?: boolean } };
// Options available on all field types
type CommonOpts = {
'ui:help'?: string;
'ui:readonly'?: boolean;
'ui:disabled'?: boolean;
'ui:classNames'?: string;
'ui:autofocus'?: boolean;
};
// --- Internal utility types ---
type UnionToIntersection<U> =
(U extends unknown ? (x: U) => void : never) extends (x: infer I) => void ? I : never;
type WidgetsFor<T, Checks> = Checks extends { when: infer M; then: { widget?: infer W } }
? T extends M ? W : never
: never;
type FieldsFor<T, Checks> = Checks extends { when: infer M; then: { field?: infer F } }
? T extends M ? F : never
: never;
type RawOptsFor<T, Checks> = UnionToIntersection<
Checks extends { when: infer M; then: infer O }
? T extends M ? Omit<O, 'widget' | 'field'> : never
: never
>;
type WithUiPrefix<T> = {
[K in keyof T as K extends string ? `ui:${K}` : never]?: T[K];
};
type ComponentOpts<T, Checks> = {
'ui:widget'?: WidgetsFor<T, Checks>;
'ui:field'?: FieldsFor<T, Checks>;
'ui:options'?: { widget?: WidgetsFor<T, Checks>; field?: FieldsFor<T, Checks> } & RawOptsFor<T, Checks>;
};
// --- Main exported type ---
// T defaults to any for vanilla usage without type param
type UiOptions<T = any, Checks = never> =
CommonOpts &
ComponentOpts<T, BaseChecks | Checks> &
WithUiPrefix<RawOptsFor<T, BaseChecks | Checks>> &
(T extends (infer U)[] ? { items?: UiOptions<U, Checks> } : {}) &
(T extends unknown[] ? {} : T extends object ? {
'ui:order'?: (keyof T | '*')[];
} & { [K in keyof T]?: UiOptions<T[K], Checks> } : {});
Vanilla Usage (No Type Parameter)
All types default T to any, so you can use them without specifying a type:
// Works without type parameter - any nested structure allowed
const simpleSchema: MyThemeUiOptions = {
foo: {
bar: {
baz: { 'ui:widget': 'CommaSeparatedWidget' },
},
},
};
// Or with explicit type for full type safety
const typedSchema: MyThemeUiOptions<{ name: string; age: number }> = {
name: { 'ui:placeholder': 'Enter name' },
age: { 'ui:widget': 'RangeWidget' },
};
Known Limitations
Record<string, T>: Index signatures conflict with ui: prefixed options. Use explicit object types instead.
- Typos in
{ when, then }: Typos like wigdet instead of widget won't be caught at definition time (only at usage).
Benefits
- Type safety: Wrong widgets/options caught at compile time
- Autocomplete: IDE suggests valid widgets and options based on field type
- Theme extensibility: Themes add widgets scoped to specific types
- User extensibility: Users add domain-specific options via marker types
- Backward compatible: Can be adopted incrementally alongside existing
UiSchema
Questions for Maintainers
- Would this fit better in
@rjsf/utils or as a separate package?
- Should
BaseChecks include all standard widgets or start minimal?
- Any concerns about the
{ when, then } naming convention?
Describe alternatives you've considered
No response
Prerequisites
What theme are you using?
core
Is your feature request related to a problem? Please describe.
Currently,
UiSchemais loosely typed. Any widget can be assigned to any field, and there's no autocomplete or validation for type-specific options likeminValue(numbers) orplaceholder(strings).Describe the solution you'd like
Core Pattern:
{ when, then }TS Playground to see implementation
Define type-specific options using a simple pattern:
Main Type
Theme Extension
Themes can add custom widgets scoped to specific types:
User Extension
Users can add domain-specific options:
Usage Examples
Key Design Decisions
Widgets use Union, Options use Intersection
When multiple checks match (e.g.,
numbermatches both base and theme checks):Both Formats Supported
Extensibility Chain
Each layer can pass through the
Checksparameter:Implementation
Full implementation (~60 lines)
Vanilla Usage (No Type Parameter)
All types default
Ttoany, so you can use them without specifying a type:Known Limitations
Record<string, T>: Index signatures conflict withui:prefixed options. Use explicit object types instead.{ when, then }: Typos likewigdetinstead ofwidgetwon't be caught at definition time (only at usage).Benefits
UiSchemaQuestions for Maintainers
@rjsf/utilsor as a separate package?BaseChecksinclude all standard widgets or start minimal?{ when, then }naming convention?Describe alternatives you've considered
No response