diff --git a/src/lib/api/adapters/base.adapter.ts b/src/lib/api/adapters/base.adapter.ts index b97cfb5d..4e2ad2d5 100644 --- a/src/lib/api/adapters/base.adapter.ts +++ b/src/lib/api/adapters/base.adapter.ts @@ -19,6 +19,7 @@ import { } from './errors' import { authStore } from '$lib/stores/auth.store' import { browser } from '$app/environment' +import { optionalProps } from '$lib/utils/typeShims' /** * Base adapter class that all resource-specific adapters extend from. @@ -124,9 +125,9 @@ export abstract class BaseAdapter { } } - // Prepare request options + // Prepare request options (filter out undefined to satisfy exactOptionalPropertyTypes) const fetchOptions: RequestInit = { - ...options, // Allow overriding defaults + ...optionalProps(options), // Allow overriding defaults, filter undefined credentials: 'include', // Still include cookies for CORS and refresh token signal: controller.signal, headers: { diff --git a/src/lib/api/adapters/resources/search.resource.svelte.ts b/src/lib/api/adapters/resources/search.resource.svelte.ts index 0d38f01f..68c8d1a1 100644 --- a/src/lib/api/adapters/resources/search.resource.svelte.ts +++ b/src/lib/api/adapters/resources/search.resource.svelte.ts @@ -113,7 +113,7 @@ export class SearchResource { this.activeRequests.set(type, controller) // Update loading state - this[type] = { ...this[type], loading: true, error: undefined } + this[type] = { ...this[type], loading: true } try { // Merge base params with provided params diff --git a/src/lib/api/resources/users.ts b/src/lib/api/resources/users.ts index 69bc6c87..d66fbf66 100644 --- a/src/lib/api/resources/users.ts +++ b/src/lib/api/resources/users.ts @@ -1,11 +1,11 @@ import { userAdapter } from '../adapters/user.adapter' export interface UserUpdateParams { - picture?: string - element?: string - gender?: number - language?: string - theme?: string + picture?: string | undefined + element?: string | undefined + gender?: number | undefined + language?: string | undefined + theme?: string | undefined } export interface UserResponse { diff --git a/src/lib/components/Navigation.svelte b/src/lib/components/Navigation.svelte index 93a93181..00c48511 100644 --- a/src/lib/components/Navigation.svelte +++ b/src/lib/components/Navigation.svelte @@ -216,7 +216,7 @@ iconOnly shape="circle" variant="primary" - element={userElement} + {...(userElement ? { element: userElement } : {})} elementStyle={Boolean(userElement)} class="new-team-button" aria-label="New team" diff --git a/src/lib/components/UserSettingsModal.svelte b/src/lib/components/UserSettingsModal.svelte index 1e246347..42781f05 100644 --- a/src/lib/components/UserSettingsModal.svelte +++ b/src/lib/components/UserSettingsModal.svelte @@ -10,6 +10,7 @@ import type { UserCookie } from '$lib/types/UserCookie' import { setUserCookie } from '$lib/auth/cookies' import { invalidateAll } from '$app/navigation' + import { optionalProps } from '$lib/utils/typeShims' interface Props { open: boolean @@ -72,14 +73,14 @@ saving = true try { - // Prepare the update data - const updateData = { + // Prepare the update data (filter undefined to satisfy exactOptionalPropertyTypes) + const updateData = optionalProps({ picture, element: currentPicture?.element, gender, language, theme - } + }) // Call API to update user settings const response = await users.update(userId, updateData) @@ -131,7 +132,12 @@ - + {#snippet children()}
{#if error} diff --git a/src/lib/composables/drag-drop.svelte.ts b/src/lib/composables/drag-drop.svelte.ts index ab978600..f1b27aa5 100644 --- a/src/lib/composables/drag-drop.svelte.ts +++ b/src/lib/composables/drag-drop.svelte.ts @@ -1,4 +1,5 @@ import type { GridCharacter, GridWeapon, GridSummon } from '$lib/types/api/party' +import { optionalProps } from '$lib/utils/typeShims' export type GridItemType = 'character' | 'weapon' | 'summon' export type GridItem = GridCharacter | GridWeapon | GridSummon @@ -30,13 +31,13 @@ export interface DragOperation { container: string position: number itemId: string - type?: GridItemType + type?: GridItemType | undefined } target: { container: string position: number - itemId?: string - type?: GridItemType + itemId?: string | undefined + type?: GridItemType | undefined } status: 'pending' | 'synced' | 'failed' retryCount: number @@ -224,12 +225,12 @@ export function createDragDropContext(handlers: DragDropHandlers = {}) { itemId: state.draggedItem.data.id, type: state.draggedItem.source.type }, - target: { + target: optionalProps({ container: state.hoveredOver.container, position: state.hoveredOver.position, - itemId: targetItem?.id || undefined, + itemId: targetItem?.id, type: state.hoveredOver.type - }, + }), status: 'pending', retryCount: 0 } diff --git a/src/lib/services/grid.service.ts b/src/lib/services/grid.service.ts index 2f9701b5..644637ec 100644 --- a/src/lib/services/grid.service.ts +++ b/src/lib/services/grid.service.ts @@ -1,6 +1,7 @@ import type { Party, GridWeapon, GridSummon, GridCharacter } from '$lib/types/api/party' import { gridAdapter } from '$lib/api/adapters/grid.adapter' import { partyAdapter } from '$lib/api/adapters/party.adapter' +import { optionalProps } from '$lib/utils/typeShims' export interface GridOperation { type: 'add' | 'replace' | 'remove' | 'move' | 'swap' @@ -38,13 +39,13 @@ export class GridService { ): Promise { try { // Note: The backend computes the correct uncap level based on the weapon's FLB/ULB/transcendence flags - const gridWeapon = await gridAdapter.createWeapon({ + const gridWeapon = await gridAdapter.createWeapon(optionalProps({ partyId, weaponId, position, mainhand: options?.mainhand, transcendenceStep: 0 - }, this.buildHeaders(editKey)) + }), this.buildHeaders(editKey)) console.log('[GridService] Created grid weapon:', gridWeapon) @@ -122,12 +123,12 @@ export class GridService { editKey?: string, options?: { shortcode?: string } ): Promise { - await gridAdapter.updateWeapon(gridWeaponId, { + await gridAdapter.updateWeapon(gridWeaponId, optionalProps({ position: updates.position, uncapLevel: updates.uncapLevel, transcendenceStep: updates.transcendenceStep, element: updates.element - }, this.buildHeaders(editKey)) + }), this.buildHeaders(editKey)) // Clear party cache if shortcode provided if (options?.shortcode) { @@ -207,14 +208,14 @@ export class GridService { options?: { main?: boolean; friend?: boolean; shortcode?: string } ): Promise { // Note: The backend computes the correct uncap level based on the summon's FLB/ULB/transcendence flags - const gridSummon = await gridAdapter.createSummon({ + const gridSummon = await gridAdapter.createSummon(optionalProps({ partyId, summonId, position, main: options?.main, friend: options?.friend, - transcendenceStage: 0 - }, this.buildHeaders(editKey)) + transcendenceStep: 0 + }), this.buildHeaders(editKey)) console.log('[GridService] Created grid summon:', gridSummon) @@ -270,12 +271,12 @@ export class GridService { editKey?: string, options?: { shortcode?: string } ): Promise { - await gridAdapter.updateSummon(gridSummonId, { + await gridAdapter.updateSummon(gridSummonId, optionalProps({ position: updates.position, quickSummon: updates.quickSummon, uncapLevel: updates.uncapLevel, - transcendenceStage: updates.transcendenceStep - }, this.buildHeaders(editKey)) + transcendenceStep: updates.transcendenceStep + }), this.buildHeaders(editKey)) // Clear party cache if shortcode provided if (options?.shortcode) { @@ -436,12 +437,12 @@ export class GridService { editKey?: string, options?: { shortcode?: string } ): Promise { - const updated = await gridAdapter.updateCharacter(gridCharacterId, { + const updated = await gridAdapter.updateCharacter(gridCharacterId, optionalProps({ position: updates.position, uncapLevel: updates.uncapLevel, transcendenceStep: updates.transcendenceStep, perpetuity: updates.perpetuity - }, this.buildHeaders(editKey)) + }), this.buildHeaders(editKey)) // Clear party cache if shortcode provided if (options?.shortcode) { diff --git a/src/lib/services/party.service.ts b/src/lib/services/party.service.ts index edf0ee9b..27c40ec4 100644 --- a/src/lib/services/party.service.ts +++ b/src/lib/services/party.service.ts @@ -2,6 +2,7 @@ import type { Party } from '$lib/types/api/party' import { partyAdapter } from '$lib/api/adapters/party.adapter' import { authStore } from '$lib/stores/auth.store' import { browser } from '$app/environment' +import { optionalProps } from '$lib/utils/typeShims' /** * Context type for party-related operations in components @@ -66,18 +67,18 @@ export class PartyService { party: Party editKey?: string }> { - const apiPayload = this.mapToApiPayload(payload) + const apiPayload = optionalProps(this.mapToApiPayload(payload)) const party = await partyAdapter.create(apiPayload) // Note: Edit key handling may need to be adjusted based on how the API returns it - return { party, editKey: undefined } + return { party } } /** * Update party details */ async update(id: string, payload: PartyUpdatePayload, editKey?: string): Promise { - const apiPayload = this.mapToApiPayload(payload) + const apiPayload = optionalProps(this.mapToApiPayload(payload)) return partyAdapter.update({ shortcode: id, ...apiPayload }) } @@ -110,7 +111,7 @@ export class PartyService { const party = await partyAdapter.remix(shortcode) // Note: Edit key handling may need to be adjusted - return { party, editKey: undefined } + return { party } } /** diff --git a/src/lib/utils/typeShims.ts b/src/lib/utils/typeShims.ts new file mode 100644 index 00000000..c3953071 --- /dev/null +++ b/src/lib/utils/typeShims.ts @@ -0,0 +1,109 @@ +/** + * Type utilities for handling exactOptionalPropertyTypes: true + * + * These utilities help work with third-party libraries and components + * that don't properly support exactOptionalPropertyTypes. + */ + +/** + * Recursively converts optional properties from { key?: T } to { key?: T | undefined } + * This is needed for libraries like bits-ui that don't include undefined in optional props. + * + * @example + * ```ts + * type Original = { optional?: string } + * type Fixed = DeepAddUndefined // { optional?: string | undefined } + * ``` + */ +export type DeepAddUndefined = + Type extends (infer Element)[] + ? DeepAddUndefined[] + : Type extends (...args: unknown[]) => unknown + ? Type + : Type extends object + ? { + [Key in keyof Type]: undefined extends Type[Key] + ? never + : DeepAddUndefined + } & { + [Key in keyof Type]?: undefined extends Type[Key] + ? undefined | DeepAddUndefined + : never + } + : Type + +/** + * Shim function that "fixes" types for exactOptionalPropertyTypes compatibility. + * Use this when passing props to third-party components that don't support exactOptionalPropertyTypes. + * + * @example + * ```ts + * + * ``` + */ +export function exactOptionalShim(value: Type): DeepAddUndefined { + return value as never +} + +/** + * Simple single-level optional property fixer. + * Faster alternative to DeepAddUndefined when you don't need recursion. + * + * @example + * ```ts + * type Original = { optional?: string; required: number } + * type Fixed = OptionalUndefined // { optional?: string | undefined; required: number } + * ``` + */ +export type OptionalUndefined = { + [Key in keyof Type as undefined extends Type[Key] ? never : Key]: Type[Key] +} & { + [Key in keyof Type as undefined extends Type[Key] ? Key : never]?: Type[Key] | undefined +} + +/** + * Helper to conditionally include optional properties only when they're defined. + * This avoids assigning undefined to optional properties. + * + * @example + * ```ts + * const props = { + * required: 'value', + * ...optionalProp('optional', maybeUndefined), + * ...optionalProp('other', maybeOther) + * } + * ``` + */ +export function optionalProp( + key: K, + value: V | undefined +): V extends undefined ? {} : { [P in K]: V } { + if (value === undefined) { + return {} as any + } + return { [key]: value } as any +} + +/** + * Helper to build objects with optional properties that are only included when defined. + * + * @example + * ```ts + * const props = optionalProps({ + * required: 'always included', + * optional: maybeUndefined, // only included if defined + * other: maybeOther // only included if defined + * }) + * ``` + */ +export function optionalProps>( + obj: T +): Partial { + const result: Partial = {} + for (const [key, value] of Object.entries(obj)) { + if (value !== undefined) { + result[key as keyof T] = value + } + } + return result +}