/** * Merge With Defaults * * Utilities for safely merging API data with form defaults. * Handles missing fields, type mismatches, and nested objects. * * @module lib/forms/utils/mergeWithDefaults */ /** * Safely get a value with type checking and default fallback * * @param value - Value to check * @param defaultValue - Default to use if value is invalid * @param typeCheck - Type checking function * @returns Valid value or default * * @example * ```ts * const temp = safeValue(apiData.temperature, 0.7, (v) => typeof v === 'number'); * ``` */ export function safeValue( value: unknown, defaultValue: T, typeCheck: (v: unknown) => v is T ): T { return typeCheck(value) ? value : defaultValue; } /** * Type guard for numbers */ export function isNumber(v: unknown): v is number { return typeof v === 'number' && !Number.isNaN(v); } /** * Type guard for strings */ export function isString(v: unknown): v is string { return typeof v === 'string'; } /** * Type guard for booleans */ export function isBoolean(v: unknown): v is boolean { return typeof v === 'boolean'; } /** * Type guard for arrays */ export function isArray(v: unknown, itemCheck?: (item: unknown) => item is T): v is T[] { if (!Array.isArray(v)) return false; if (itemCheck) return v.every(itemCheck); return true; } /** * Type guard for objects (non-null, non-array) */ export function isObject(v: unknown): v is Record { return typeof v === 'object' && v !== null && !Array.isArray(v); } /** * Deep merge two objects, with source values taking precedence * Only merges values that pass type checking against defaults * * @param defaults - Default values (used as type template) * @param source - Source values to merge (from API) * @returns Merged object with all fields from defaults * * @example * ```ts * const defaults = { temperature: 0.7, max_tokens: 8192, top_p: 0.95 }; * const apiData = { temperature: 0.5 }; // missing max_tokens and top_p * const merged = deepMergeWithDefaults(defaults, apiData); * // { temperature: 0.5, max_tokens: 8192, top_p: 0.95 } * ``` */ export function deepMergeWithDefaults>( defaults: T, source: Partial | null | undefined ): T { if (!source) return { ...defaults }; const result = { ...defaults } as T; for (const key of Object.keys(defaults) as Array) { const defaultValue = defaults[key]; const sourceValue = source[key]; // Skip if source doesn't have this key if (!(key in source) || sourceValue === undefined) { continue; } // Handle nested objects recursively if (isObject(defaultValue) && isObject(sourceValue)) { result[key] = deepMergeWithDefaults( defaultValue as Record, sourceValue as Record ) as T[keyof T]; continue; } // For primitives and arrays, only use source if types match if (typeof sourceValue === typeof defaultValue) { result[key] = sourceValue as T[keyof T]; } // Special case: default is null but source has a value (nullable fields) else if (defaultValue === null && sourceValue !== null) { result[key] = sourceValue as T[keyof T]; } // Special case: allow null for nullable fields else if (sourceValue === null && defaultValue === null) { result[key] = null as T[keyof T]; } } return result; } /** * Create a form values initializer from API data * * This is a higher-order function that creates a type-safe initializer * for transforming API responses into form values with defaults. * * @param defaults - Default form values * @param transform - Optional transform function for custom mapping * @returns Function that takes API data and returns form values * * @example * ```ts * const initializeAgentForm = createFormInitializer( * defaultAgentTypeValues, * (apiData, defaults) => ({ * ...defaults, * name: apiData?.name ?? defaults.name, * model_params: deepMergeWithDefaults( * defaults.model_params, * apiData?.model_params * ), * }) * ); * * // Usage * const formValues = initializeAgentForm(apiResponse); * ``` */ export function createFormInitializer>( defaults: TForm, transform?: (apiData: TApi | null | undefined, defaults: TForm) => TForm ): (apiData: TApi | null | undefined) => TForm { return (apiData) => { if (transform) { return transform(apiData, defaults); } // Default behavior: deep merge return deepMergeWithDefaults( defaults as Record, apiData as Record | null | undefined ) as TForm; }; }