KMP-Generated Kotlin — Idiosyncrasies for JS/TS Consumers
When the Solibo SDK is compiled to JavaScript via Kotlin Multiplatform, certain Kotlin types don’t map 1:1 to JS primitives. The SoliboClient facade handles the common conversions; this document also covers the remaining raw interop cases.
Quick reference
| Kotlin type | Facade/query result | Raw interop |
|---|---|---|
List<T> / KtList<T> | JS arrays | KtList.fromJsArray(arr) |
List<Long> / KtList<bigint> | JS arrays; ID inputs accept number \| string \| bigint | KtList.fromJsArray(ids.map(BigInt)) |
Map<K,V> / KtMap | JS Map | KtMap.fromJsMap(map) |
Long (API parameter) | number \| string \| bigint | BigInt(value) |
Long (JSON serialization) | Handled — BigInt polyfill converts to string | n/a |
Instant | ISO-8601 string | createInstant(date) |
LocalDate | ISO date string (YYYY-MM-DD) | createLocalDate(year, month, day) |
enum class | .name (string key) or .value (string value) | Pass SDK enum constant directly |
| Sealed / polymorphic | Check .type string discriminator | n/a |
Prefer the facade
SoliboClient accepts ordinary JavaScript values and performs the KMP conversion internally:
import { createSoliboClient } from '@solibo/solibo-sdk'
const client = createSoliboClient({
auth: { kind: 'browser', userPoolId: '...', clientId: '...' },
})
const tasks = await client.api.task.indexTasks({ companyId })
Use client.raw or @solibo/solibo-sdk/interop only when you need response envelopes, request headers, redirects, downloads, or direct Kotlin collection construction.
1. Paged Lists — PagedList<T>
All collection endpoints return a PagedList<T> instead of a plain list. The wrapper has three fields:
| Field | Type | Description |
|---|---|---|
.items | KtList<T> | The current page of results |
.meta | Meta | Metadata — .count is the number of items on this page |
.paging | Paging | Cursor tokens — .next / .previous for navigation; .next === "END_OF_LIST" means no more pages |
In hooks (solibo-react)
Paged list hooks use useInfiniteQuery. Data is accumulated across pages in data.pages:
const { data, fetchNextPage, hasNextPage } = useIssues({ companyId })
// Flatten all pages into one array
const allIssues = data?.pages.flatMap(page => page.items) ?? []
Each page in data.pages is already a plain JS object — page.items is a readonly T[] array because SoliboClient normalizes KMP collections at the SDK boundary.
Directly from the SDK (outside a hook)
const body = await client.api.task.indexTasks({ companyId })
const tasks = body.items // T[]
const nextToken = body.paging.next // string | null
2. Collections — KtList / KtMap
Reading (queries)
solibo-query receives normalized results from SoliboClient, so React components get plain arrays without importing or invoking bridge helpers.
If you work directly with the facade outside a hook, collection fields are already arrays:
const response = await client.api.task.indexTasks({ companyId })
const items = response.items
// items is a readonly T[] — iterate, map, find, etc.
Manual KtList conversion is only needed after an explicit client.raw call.
Writing (commands)
Facade command inputs accept plain arrays:
client.api.sections.multiCreateTagOnSection({ companyId, sectionId, input: tags })
Raw callers construct KMP collections explicitly:
import { KtList } from '@solibo/solibo-sdk/interop'
client.raw.api.thirdParty.sendWithDigiPost(
BigInt(companyId),
BigInt(thirdPartyId),
KtList.fromJsArray(publishTo.map(BigInt))
)
For maps:
import { KtMap } from '@solibo/solibo-sdk/interop'
const ktMap = KtMap.fromJsMap(new Map(Object.entries(myObj)))
Use KtMutableList.fromJsArray() / KtMutableMap.fromJsMap() when the API requires a mutable variant.
3. Long — API parameters
Kotlin Long becomes a bigint on the raw JS side. Facade IDs accept number, string, or bigint and convert internally:
client.api.organization.showOrganization({ organizationId })
client.api.task.indexTasks({ companyId })
// Raw positional interop still requires bigint:
client.raw.api.task.indexTasks(BigInt(companyId))
IDs returned in responses are also bigint. When comparing or using them as React Query keys, convert to string or number as needed:
const id = response.id.toString() // safe for display, keys, URL params
const idNum = Number(response.id) // only safe for values < 2^53
Serialization (handled automatically)
solibo-query ships a bigint-polyfill.ts that patches BigInt.prototype.toJSON to serialize as a string. This is imported at package entry and requires no action from consumers.
4. Date and time — Instant and LocalDate
Facade inputs and results use ISO strings for Kotlin date/time values:
// For LocalDate fields (e.g. startDate, endDate):
'2025-01-31'
// For Instant fields (e.g. validFrom, closedAt):
new Date().toISOString()
When reading date fields from responses, convert to a JS Date for display:
const date = new Date(response.createdAt)
Do not pass a raw Date object to the facade. Use toISOString() for Instant fields and a YYYY-MM-DD string for LocalDate fields. Raw KMP callers can construct the corresponding values with createInstant(date) and createLocalDate(year, month, day) from @solibo/solibo-sdk/interop.
5. Enums
Kotlin enums compile to singleton objects, not plain strings. Two properties are available:
.name— the enum entry name as declared in Kotlin ("APPROVED","FOR_APPROVAL").value— the JSON serialization value (usually the same string)
// Reading:
expense.status.name === 'APPROVED' // true
expense.status.value === 'APPROVED' // true (usually identical)
// Comparing with SDK constant (preferred — survives renames):
expense.status === ExpenseStatus.APPROVED
// Facade inputs accept either the enum constant or its documented string value:
client.api.expense.createExpense({
companyId,
input: { status: ExpenseStatus.FOR_APPROVAL, /* ... */ },
})
Do not compare a returned enum object directly with a raw string using ===; use .name, .value, or the SDK constant.
6. Sealed classes / polymorphic types
The API returns polymorphic types (e.g. Resident is a sealed superclass; PersonResident and OrgResident extend it). The concrete type is identified by a type field whose value is the fully-qualified Kotlin class name:
if (resident.type === 'solibo.residents.query.domain.PersonResident') {
// narrow to PersonResident — access .email, .name, .mobile, etc.
}
instanceof checks do not work across the Kotlin/JS boundary.
7. null vs undefined
Kotlin nullable fields (String?, Long?, etc.) arrive as null in JS, not undefined. Use null-coalescing or loose null checks:
resident.email ?? 'fallback' // correct
resident.email !== undefined // may miss null — incorrect
resident.email != null // correct (covers both)