1+ /**
2+ * Represents an option object with a value and optional label.
3+ * Used by react-select and similar components.
4+ */
15export interface OptionWithValue {
6+ /** The actual value of the option (string or number) */
27 value : string | number ;
8+ /** Optional display label for the option */
39 label ?: string ;
4- __lv ?: any ;
10+ /** Internal wrapper object (used by form handling logic) */
11+ __lv ?: unknown ;
512}
613
14+ /**
15+ * Type guard to check if a value is a string.
16+ * @param value - The value to check
17+ * @returns true if the value is a string
18+ */
719export function isString ( value : unknown ) : value is string {
820 return typeof value === "string" ;
921}
1022
23+ /**
24+ * Type guard to check if a value is a valid OptionWithValue object.
25+ * Validates that the object has a 'value' property that is either a string or number.
26+ * @param value - The value to check
27+ * @returns true if the value is a valid OptionWithValue
28+ */
1129export function isOptionWithValue ( value : unknown ) : value is OptionWithValue {
1230 return (
1331 value !== null &&
1432 typeof value === "object" &&
1533 ! Array . isArray ( value ) &&
16- "value" in value
34+ "value" in value &&
35+ ( typeof ( value as Record < string , unknown > ) . value === "string" || typeof ( value as Record < string , unknown > ) . value === "number" )
1736 ) ;
1837}
1938
39+ /**
40+ * Type guard to check if a value is an array of strings.
41+ * @param value - The value to check
42+ * @returns true if the value is a string array
43+ */
2044export function isStringArray ( value : unknown ) : value is string [ ] {
2145 return Array . isArray ( value ) && value . every ( ( item ) => typeof item === "string" ) ;
2246}
2347
48+ /**
49+ * Type guard to check if a value is an array of OptionWithValue objects.
50+ * @param value - The value to check
51+ * @returns true if the value is an array of valid OptionWithValue objects
52+ */
2453export function isOptionArray ( value : unknown ) : value is OptionWithValue [ ] {
2554 return Array . isArray ( value ) && value . every ( ( item ) => isOptionWithValue ( item ) ) ;
2655}
2756
57+ /**
58+ * Normalizes an unknown value into either a string or OptionWithValue.
59+ * Used for basic option processing where the input format is uncertain.
60+ * @param option - The option to normalize
61+ * @returns A normalized string or OptionWithValue object
62+ */
2863export function normalizeOption ( option : unknown ) : OptionWithValue | string {
2964 if ( isString ( option ) ) {
3065 return option ;
@@ -35,9 +70,106 @@ export function normalizeOption(option: unknown): OptionWithValue | string {
3570 return String ( option ) ;
3671}
3772
73+ /**
74+ * Normalizes an array of unknown values into an array of strings or OptionWithValue objects.
75+ * Handles cases where the input might not be an array by returning an empty array.
76+ * @param options - The options array to normalize
77+ * @returns An array of normalized options
78+ */
3879export function normalizeOptions ( options : unknown ) : Array < OptionWithValue | string > {
3980 if ( ! Array . isArray ( options ) ) {
4081 return [ ] ;
4182 }
4283 return options . map ( normalizeOption ) ;
4384}
85+
86+ /**
87+ * Sanitizes an option to ensure it has proper primitive values for label/value.
88+ * This is the main utility for processing complex nested option structures that can
89+ * come from various sources (APIs, form data, etc.) into a format compatible with react-select.
90+ *
91+ * Handles multiple nesting scenarios:
92+ * 1. String options: returned as-is (e.g., "simple-option")
93+ * 2. __lv wrapper objects: extracts inner option from {__lv: {label: "...", value: "..."}}
94+ * 3. Nested label/value objects: handles {label: {label: "Documents"}, value: {value: "123"}}
95+ *
96+ * This function was created to fix React error #31 where nested objects were being
97+ * passed to React components that expected primitive values.
98+ *
99+ * @param option - The option to sanitize (can be string, object, or complex nested structure)
100+ * @returns A clean option with primitive label/value or a string
101+ *
102+ * @example
103+ * // Simple string
104+ * sanitizeOption("hello") // returns "hello"
105+ *
106+ * @example
107+ * // Nested object structure
108+ * sanitizeOption({
109+ * label: {label: "Documents", value: "123"},
110+ * value: {label: "Documents", value: "123"}
111+ * }) // returns {label: "Documents", value: "123"}
112+ *
113+ * @example
114+ * // __lv wrapper
115+ * sanitizeOption({
116+ * __lv: {label: "Test", value: "test-id"}
117+ * }) // returns {label: "Test", value: "test-id"}
118+ */
119+ export function sanitizeOption ( option : unknown ) : { label : string ; value : unknown } | string {
120+ if ( typeof option === "string" ) return option ;
121+
122+ if ( ! option || typeof option !== "object" ) {
123+ return {
124+ label : "" ,
125+ value : "" ,
126+ } ;
127+ }
128+
129+ // If option has __lv wrapper, extract the inner option
130+ if ( "__lv" in option ) {
131+ const innerOption = ( option as Record < string , unknown > ) . __lv ;
132+
133+ let actualLabel = "" ;
134+ let actualValue = innerOption ?. value ;
135+
136+ // Handle nested label in __lv
137+ if ( innerOption ?. label && typeof innerOption . label === "object" && "label" in innerOption . label ) {
138+ actualLabel = String ( innerOption . label . label || "" ) ;
139+ } else {
140+ actualLabel = String ( innerOption ?. label || innerOption ?. value || "" ) ;
141+ }
142+
143+ // Handle nested value in __lv
144+ if ( innerOption ?. value && typeof innerOption . value === "object" && "value" in innerOption . value ) {
145+ actualValue = innerOption . value . value ;
146+ }
147+
148+ return {
149+ label : actualLabel ,
150+ value : actualValue ,
151+ } ;
152+ }
153+
154+ // Handle nested label and value objects
155+ const optionObj = option as Record < string , unknown > ;
156+ let actualLabel = "" ;
157+ let actualValue = optionObj . value ;
158+
159+ // Extract nested label
160+ if ( optionObj . label && typeof optionObj . label === "object" && "label" in optionObj . label ) {
161+ actualLabel = String ( optionObj . label . label || "" ) ;
162+ } else {
163+ actualLabel = String ( optionObj . label || optionObj . value || "" ) ;
164+ }
165+
166+ // Extract nested value
167+ if ( optionObj . value && typeof optionObj . value === "object" && "value" in optionObj . value ) {
168+ actualValue = optionObj . value . value ;
169+ }
170+
171+ return {
172+ label : actualLabel ,
173+ value : actualValue ,
174+ } ;
175+ }
0 commit comments