fix: apply exactOptionalPropertyTypes shims to adapters and services

- Add optionalProps shim to base.adapter.ts for RequestInit compatibility
- Apply optionalProps to UserSettingsModal updateData
- Use conditional spreading for Navigation Button element prop
- Apply optionalProps to drag-drop target object creation
- Apply optionalProps to party.service create/update methods
- Apply optionalProps to grid.service CRUD operations
- Fix transcendenceStage -> transcendenceStep typo in grid.service
- Update UserUpdateParams interface to include | undefined
- Update DragOperation interface properties to include | undefined

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude <noreply@anthropic.com>
This commit is contained in:
Justin Edmund 2025-11-28 18:42:38 -08:00
parent 16e24e337b
commit a74653ee93
9 changed files with 154 additions and 35 deletions

View file

@ -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: {

View file

@ -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

View file

@ -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 {

View file

@ -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"

View file

@ -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 @@
</script>
<Dialog bind:open {onOpenChange} title="@{username}" description="Account Settings">
<Dialog
bind:open
{...(onOpenChange ? { onOpenChange } : {})}
title="@{username}"
description="Account Settings"
>
{#snippet children()}
<form onsubmit={handleSave} class="settings-form">
{#if error}

View file

@ -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
}

View file

@ -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<GridUpdateResult> {
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<Party | null> {
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<Party> {
// 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<Party | null> {
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<GridCharacter | null> {
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) {

View file

@ -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<Party> {
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 }
}
/**

109
src/lib/utils/typeShims.ts Normal file
View file

@ -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<Original> // { optional?: string | undefined }
* ```
*/
export type DeepAddUndefined<Type> =
Type extends (infer Element)[]
? DeepAddUndefined<Element>[]
: Type extends (...args: unknown[]) => unknown
? Type
: Type extends object
? {
[Key in keyof Type]: undefined extends Type[Key]
? never
: DeepAddUndefined<Type[Key]>
} & {
[Key in keyof Type]?: undefined extends Type[Key]
? undefined | DeepAddUndefined<Type[Key]>
: 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
* <Dialog {...exactOptionalShim({ onOpenChange: myHandler })} />
* ```
*/
export function exactOptionalShim<Type>(value: Type): DeepAddUndefined<Type> {
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<Original> // { optional?: string | undefined; required: number }
* ```
*/
export type OptionalUndefined<Type> = {
[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<K extends string, V>(
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<T extends Record<string, unknown>>(
obj: T
): Partial<T> {
const result: Partial<T> = {}
for (const [key, value] of Object.entries(obj)) {
if (value !== undefined) {
result[key as keyof T] = value
}
}
return result
}