Skip to content

feat!: remove hand-written types and API wrappers - #1836

Merged
isekovanic merged 19 commits into
release-v10from
reduce-hand-written-types
Aug 21, 2026
Merged

feat!: remove hand-written types and API wrappers#1836
isekovanic merged 19 commits into
release-v10from
reduce-hand-written-types

Conversation

@szuperaz

@szuperaz szuperaz commented Aug 19, 2026

Copy link
Copy Markdown
Contributor

Breaking changes

  • Several type renames (hand-written types replaced with generated ones)
  • Moderation endpoints migrated from hand-written API v1 to generated API v2
  • Permission v1 remainders removed

The full list of changes is in the migration guides

Description of the changes, What, Why and How?

  • Remove hand-written API types
  • Migrate the few hand-written API calls to generated API -> mainly moderation endpoints
  • Removed 3 "proxy" methods from channel: channel.getReplies, channel.search and channel.getReactions -> technically none of these have a channel cid param, but having them on channel is handy, but my logic here was that if we start adding these methods to channel, it's something we have to maintain manually when new API calls introduced by regeneration
  • Removed the permission v1 remainders

https://linear.app/stream/issue/REACT-1066/remove-unnecessary-types-from-typests
https://linear.app/stream/issue/REACT-1065/check-poll-types-in-stream-chat-js
https://linear.app/stream/issue/REACT-1087/check-few-api-methods-bypass-the-generated-layer

szuperaz and others added 6 commits August 19, 2026 13:37
`OwnUserBase` hand-listed the fields that exist on `OwnUserResponse` but not on
`UserResponse`. `client._handleUserEvent` turns that list into a runtime lookup
(`isOwnUserBaseProperty`) and uses it to decide which keys survive a `user.updated`
event — so a field missing from the list is deleted off `client.user`.

The list had drifted from the spec in both directions: it omitted
`latest_hidden_channels` and carried a phantom `roles` that `OwnUserResponse` has
never had. Deriving the type makes the two impossible to desynchronise.

Also drops two stale `Omit` keys and one dead helper found alongside it.

BREAKING CHANGES:

* `Device`, `DeviceFields` and `BaseDeviceFields` are removed. Use the generated
  `DeviceResponse`. The shapes differ: `created_at` is `Date` (was `string` — the
  decoders always produced a `Date`, so the old annotation was wrong),
  `push_provider` widens to `string`, `user_id` is required, `provider` and `user`
  are gone, and `hardware_id` / `voip` are new.

* `OwnUserBase` keeps its name but changes shape. It gains
  `latest_hidden_channels?: Array<string>`, loses `roles?: string[]` (a field
  `OwnUserResponse` does not have — reads always returned `undefined`; the nearest
  real field is `teams_role`), types `devices` as `Array<DeviceResponse>`, and drops
  `| null` from `total_unread_count_by_team`.

* `channel._channelURL()` is removed with no replacement. It built a URL string for
  the hand-rolled request layer that no longer exists; nothing in the SDK called it.

BEHAVIOUR FIX:

* `client.user.latest_hidden_channels` is no longer deleted on every `user.updated`
  event for the connected user. Because a `user.updated` body is a plain
  `UserResponse` and the hand-written list omitted the field, it was pruned on every
  such event and read back as `undefined` regardless of server state.

NON-BREAKING (both widen):

* `ChannelUpdateOptions` no longer omits `'members'` from `UpdateChannelRequest` —
  that key does not exist on the request (it has `add_members` / `remove_members`),
  so the omit was a silent no-op.
* `PinnedMessagePaginationOptions` no longer omits `'member_custom_include'`. The
  endpoint accepts it, so omitting it was narrowing the API.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Twenty-one exported types in `types.ts` described admin surface that left this
package when the server-side API moved to `@stream-io/node-sdk` — push-provider
credentials, permission policies, blocklists, channel-type config. None had a
reference anywhere in `src`, and no endpoint in this SDK returns them.

Two more were restating a generated union rather than deriving from it, so they are
now read off `ChannelConfigWithInfo` instead of deleted.

BREAKING CHANGES:

* Removed with no replacement: `APNConfig`, `AsyncModerationOptions`, `BlockList`,
  `CommandVariants`, `FirebaseConfig`, `GetRepliesRequest`, `GiphyVersions`,
  `HuaweiConfig`, `Policy`, `PolicyRequest`, `Product`, `PushProviderAPN`,
  `PushProviderCommon`, `PushProviderConfig`, `PushProviderFirebase`,
  `PushProviderHuawei`, `PushProviderID`, `PushProviderXiaomi`, `UR`,
  `VotesFiltersOptions`, `XiaomiConfig`.

* `GetRepliesAPIResponse` is removed. Use the generated `GetRepliesResponse`. It was
  `APIResponse & { messages: MessageResponse[] }` with no reference in `src`; the
  generated shape is what `client.getReplies()` actually resolves to, wrapped in
  `StreamResponse<…>` so it also carries `metadata`.

* `Product` was an `enum`, i.e. a runtime value in the bundle — not just a type.
  `import { Product } from 'stream-chat'` now fails at runtime, not only at compile
  time. Inline the string: `'chat'`, `'video'`, `'moderation'`, `'feeds'`.

* `UR` (`Record<string, unknown>`) was a v9 type utility with no remaining callers.
  Inline `Record<string, unknown>`.

* `Automod` and `AutomodBehavior` are NARROWED. They are now
  `ChannelConfigWithInfo['automod']` and `ChannelConfigWithInfo['automod_behavior']`
  — exactly `'disabled' | 'simple' | 'AI'` and `'flag' | 'block' | 'shadow_block'`.
  Both previously carried a `| (string & {})` tail, so they accepted any string and
  the documented values were a hint rather than a constraint. Assigning an arbitrary
  string now fails to compile. Reads of `channel.getConfig().automod` are unaffected.

KEPT deliberately, despite having no reference in `src`:

* `PushProvider` — derives from `CreateDeviceRequest['push_provider']` and names the
  union `client.createDevice()` accepts.
* `ThreadFilters`, `TranslationLanguage` — derived aliases documented as v10 targets
  for v9 renames, and part of the `*Filters` family that derives per-endpoint
  operator constraints from the request types.

Note: `test/typescript/unit-test.ts` still imports `PolicyRequest` and `UR`. That
harness is already broken independently (it calls 32 client methods removed in the
server-side split) and is out of scope for this PR.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Six exported types were structurally identical to something `src/gen` already emits,
verified by compiling mutual-assignability assertions rather than by inspection. Four
more were hand-written copies of request-type field sets; those keep their names but
are derived now, so a spec change updates them instead of drifting past them.

The twelve sort aliases were all exactly `SortParamRequest[]` — twelve names for one
type. Unlike the `*Filters` aliases, which resolve to per-endpoint `Filters<{...}>`
shapes carrying that endpoint's declared operators, a sort alias narrowed nothing.

BREAKING CHANGES:

* Removed, replacement is structurally identical (pure find/replace):
  - `ChannelData` -> `ChannelInput`. Was
    `ReplacePropertyTypes<ChannelInput, { custom: CustomChannelData }>`, but
    `ChannelInput.custom` is already `CustomChannelData`, so the mapped type was a
    no-op.
  - `PollResponse_old` -> `PollResponseData`. Was `PollResponseData & PollEnrichData`;
    all six `PollEnrichData` fields are already on `PollResponseData`.
  - `PollEnrichData` -> `PollResponseData`. Fully subsumed.
  - `LiveLocationPayload` -> `SharedLocation`. Was
    `RequireLiteral<SharedLocation, 'end_at'>`, and its only consumer immediately did
    `Omit<…, 'end_at'>`, undoing the requirement.
  - `Pager` -> the request type's own `limit` / `next` / `prev`.
  - `ReplacePropertyTypes` -> none. Type utility whose last consumer was `ChannelData`.

* All twelve sort aliases are removed: `BannedUsersSort`, `ChannelSort`, `DraftSort`,
  `MemberSort`, `PinnedMessagesSort`, `PollSort`, `ReactionSort`, `ReminderSort`,
  `SearchMessageSort`, `ThreadSort`, `UserSort`, `VoteSort`. Use `SortParamRequest[]`.
  Note the brackets — the alias WAS the array, so `ChannelSort` becomes
  `SortParamRequest[]`, not `SortParamRequest`. Type-only; no runtime change.

* `ChannelOptions` keeps its name, changes shape. Now
  `Omit<QueryChannelsRequest, 'filter_conditions' | 'sort'>`. It GAINS
  `member_custom_include?: Array<string>` (the endpoint has always accepted it; the
  hand copy never mirrored it) and LOSES `user_id?: string`, which
  `QueryChannelsRequest` does not have — anything set there was silently dropped.

* `UserOptions`, `QueryPollsOptions`, `QueryVotesOptions` keep their names and are now
  derived (`Omit<QueryUsersPayload, 'filter_conditions' | 'sort'>`,
  `Omit<QueryPollsRequest, 'filter' | 'sort'>`,
  `Omit<QueryPollVotesRequest, 'filter' | 'sort'>`). All three are field-for-field
  what they were; deriving them means they can no longer drift.

KEPT deliberately:

* `ChannelUpdateOptions` and the `*Filters` family. Both were already derived, so they
  restate nothing and self-update. A filter alias also carries real per-endpoint
  information (its declared operators) that a sort alias never did.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
`APIResponse` was `{ duration: string }` — the response envelope from before the
generated layer existed. Every generated response already carries `duration`, and the
transport wraps results in `StreamResponse<T>`, which also carries `metadata`. So the
aliases built on it were not merely redundant, they were weaker than the real return
type.

`UpdatedMessage` built a request type by subtracting a hand-maintained constant from a
response type. The generated `MessageRequest` already is that shape, and is correct
where `UpdatedMessage` was not.

BREAKING CHANGES:

* Removed from the `APIResponse` family — replacements are reached through
  `StreamResponse<…>` when they are method return values, so each gains a required
  `metadata` field:
  - `SearchAPIResponse` -> `SearchResponse`. `results` entries are `SearchResult`
    rather than an inline `{ message }`.
  - `SendFileAPIResponse` -> `FileUploadResponse` / `ImageUploadResponse`.
  - `UpdateChannelAPIResponse` -> `UpdateChannelResponse`.
  - `UsersAPIResponse` -> `UpdateUsersResponse` / `QueryUsersResponse`.
  - `TaskResponse` -> the endpoint's own response type.
  - `ReactionAPIResponse` -> `SendReactionResponse` / `DeleteReactionResponse`.
  - `Flag` and `FlagDetails` -> `FlagDetailsResponse`.
  Code that only destructures the payload is unaffected; code that annotates a
  variable with a removed alias needs the new name.

* `UpdatedMessage` -> `MessageRequest`. This TIGHTENS what compiles, deliberately:
  - `MessageRequest['type']` is `'regular' | 'system'`, where `UpdatedMessage['type']`
    was the six-member `MessageLabel` including `'deleted'`, `'error'`, `'ephemeral'`
    and `'reply'` — none of which a client may send.
  - Server-owned `MessageResponse` fields absent from the reserved list (`cid`,
    `shadowed`, `reaction_groups`, …) were assignable to an update payload. They are
    not on `MessageRequest`.

* `MessageLabel` and `ReservedUpdatedMessageFields` are removed with it. The runtime
  constant `RESERVED_UPDATED_MESSAGE_FIELDS` stays — `toUpdatedMessagePayload()` still
  uses it to strip server-owned keys off a `LocalMessage`; it just no longer drives a
  type.

* `MessageComposerMiddlewareState.message` is now `MessageRequest`, not
  `MessageRequest | UpdatedMessage`. Custom composer middleware that annotated the
  union should drop the `UpdatedMessage` arm.

NOT removed, and why:

* `APIResponse`, `FlagMessageResponse`, `FlagUserResponse`, `MuteUserResponse` and
  `UnmuteUserResponse` survive. Every remaining reference to them sits inside the
  hand-written `/moderation/*` methods on `StreamChat` that bypass the generated
  client. Those methods are migrating in a separate PR and these types go with them.
  The one `APIResponse` use that was NOT pinned — the `deleteDraft` offline-queue
  generic in `channel.ts` — is switched to
  `Awaited<ReturnType<ChannelApi['deleteDraft']>>`, matching the neighbouring
  `createDraft` queue call.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Six methods either forwarded their whole body to another one or ran a runtime check
that restated something the type system already enforces. Each was a signature that
had to be re-checked by hand after a regeneration in exchange for nothing.

BREAKING CHANGES:

* `client.queryBannedUsers(...)` is removed. Its entire body was
  `return await super.queryBannedUsers(...args)` and it was not marked `override`, so
  the inherited `ChatApi.queryBannedUsers` you were already reaching is unchanged. No
  call-site change needed.

* `client.partialUpdateThread(messageId, partialThreadObject, requestOptions?)` is
  removed. Use `client.updateThreadPartial({ message_id, set, unset }, options?)`.
  The `PartialThreadUpdate` type goes with it — `UpdateThreadPartialRequest` is the
  replacement.

  - The reserved-field guard is gone, and it was wrong in both directions. It rejected
    `id`, `type`, `user` and `participants` — none of which are fields on
    `ThreadResponse`, so legitimate custom fields with those names were blocked — while
    letting through `parent_message_id`, `channel_cid`, `created_by_user_id`,
    `thread_participants`, `reply_count`, `participant_count`,
    `active_participant_count` and `deleted_at`, all of which ARE server-owned.
    A rejected write now surfaces as a rejected promise instead of a synchronous
    `throw`; adjust any try/catch that expected the latter.
  - The empty-`messageId` check is gone. `message_id` is required on
    `UpdateThreadPartialRequest`, so it is a compile error now.

* `channel.search(...)` is removed. Use `client.search(...)`. The removed method
  forwarded to `client.search()` WITHOUT scoping the query to the channel — despite
  the name it searched every channel the user could see. `client.search()` is the
  identical call. If you assumed it was channel-scoped, add the scope to your filter;
  that is a bug fix in the integration, not a regression here.

* `channel.getReplies(...)` is removed. Use `client.getReplies(...)`. Pure forward —
  the removed method's own comment noted it did nothing with the result.

* `channel.getReactions(...)` is removed. Use `client.getReactions(...)`. Pure forward.

* `channel.sendAction(...)` is kept, but its `Message ID is missing` guard is gone.
  `runMessageAction` requires `id: string`, so an empty id is a compile error; an empty
  string at runtime reaches the server and is rejected there.

Internal: `MessageIntervalPaginator` now calls `channel.getClient().getReplies(...)`
directly. Unit tests that stubbed `channel.getReplies` or `channel.search` were
retargeted at the client, which is the real seam.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
@szuperaz szuperaz changed the title Reduce hand written types feat: reduce hand-written types and API wrappers Aug 19, 2026
szuperaz and others added 2 commits August 20, 2026 08:26
A sweep of `stream-chat-react` and `stream-chat-react-native` for every type removed
earlier on this branch found that two of them have real downstream consumers and no
generated equivalent. Removing them does not delete a hand-written type, it relocates one
into two repos — the opposite of the goal.

* `GiphyVersions` is `keyof Images`, derived from the generated attachment-images shape,
  so it cannot drift. It is the same pattern as `PushProvider`
  (`CreateDeviceRequest['push_provider']`), which was deliberately kept for exactly this
  reason, so removing this one was inconsistent. `stream-chat-react` exposes it on two
  public types — `AttachmentProps.giphyVersion` and
  `AttachmentContextValue.giphyVersion` — across 6 sites in 3 files.

* `MessageLabel` has no generated substitute: `MessageResponse['type']` is a bare
  `string`, so every replacement widens rather than narrows. It was removed only as
  collateral of the `UpdatedMessage` retirement, and both SDKs use it as a discriminant —
  `stream-chat-react-native` types its SQLite message and draft-message rows with it
  (4 files), `stream-chat-react` types the `DateSeparatorMessage` arm of its exported
  `RenderedMessage` union with it.

`CommandVariants` stays removed. Unlike these two it is genuinely hand-written — eight
literals plus `keyof CustomCommandData`, with no generated backing — so it is what this
effort targets. Its two React Native call sites are a cast on a `string` and an icon-name
prop, both better served locally.

This reverses part of two earlier commits on this branch; nothing was released in
between.

BREAKING CHANGES:

* None. This restores two previously-removed exports; it removes nothing and narrows
  nothing.

Notes on what did NOT come back:

* `UpdatedMessage` stays removed, and `MessageLabel` is still not valid as a write
  payload type — `MessageRequest['type']` is `'regular' | 'system'`. `MessageLabel`
  is for typing `type` values on the read side only.
* `ReservedUpdatedMessageFields` stays removed. The runtime constant
  `RESERVED_UPDATED_MESSAGE_FIELDS` is unaffected.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
The `/moderation/*` methods on `StreamChat` were the last endpoints in the SDK that built
their own request instead of calling the generated client — nine hand-rolled
`this.api.post(this.baseURL + '/moderation/...')` calls that had to be re-checked by hand
after every regeneration.

Four of them have a generated V2 equivalent, so the wrapper only reshaped arguments and is
removed rather than rewritten. Four more had no caller in `stream-chat-react` or
`stream-chat-react-native` and are dropped. One has no generated equivalent and stays.

After this, the only code outside `src/gen` that builds its own request is `unbanUser` and
the `/hi` telemetry ping.

BREAKING CHANGES:

* Removed from `StreamChat`; use the generated V2 method via `client.moderation`:
  - `banUser(id, options?)`      -> `moderation.ban({ target_user_id: id, ...options })`
  - `muteUser(id, options?)`     -> `moderation.mute({ target_ids: [id], ...options })`
  - `unmuteUser(id)`             -> `moderation.unmute({ target_ids: [id] })`
  - `flagMessage(id, options?)`  -> `moderation.flagMessage(id, reason?, options?)`
  Note mute/unmute take `target_ids` as an ARRAY; a single id becomes `[id]`.

* Return types change. These resolve to `StreamResponse<…>` of the generated response, so
  they gain `metadata`. Two lose fields: the mute response no longer carries `mute`
  (singular) — use `mutes` — and the flag response is `FlagItemResponse
  { duration, item_id }` rather than a nested `flag` object. Code that only awaits these
  calls is unaffected; neither downstream SDK read them.

* Two ban options are gone with no replacement: V2 `BanRequest` has no `delete_reactions`
  and no `ban_from_future_channels`. Verified unused by both downstream SDKs, neither of
  which passes any ban option at all.

* `BanUserOptions` is now `Omit<BanRequest, 'target_user_id'>` — derived, so a spec change
  updates it instead of drifting past it. `MuteUserOptions` is removed (V2 mute accepts
  only `timeout`), as are `MessageDeletionStrategy`, `MuteUserResponse`,
  `FlagMessageResponse`, `FlagUserResponse` and `UnmuteUserResponse`.

* Removed with no replacement, none of them used by either downstream SDK:
  `client.flagUser` (use `client.moderation.flagUser`), `client.unflagMessage`,
  `client.unflagUser` and `client.unblockMessage` (V2 has no unflag or unblock-message
  endpoint).

* `shadowBan` / `removeShadowBan` are removed from BOTH `StreamChat` and `Channel`. They
  were sugar for a flag that is still public, so the capability is intact:
  `channel.banUser(id, { shadow: true })`, `channel.unbanUser(id, { shadow: true })`.

* `channel.banUser` keeps its signature but its options no longer accept `channel_cid` —
  the channel sets it, so passing one was a silent no-op.

* `Moderation.flagUser` / `Moderation.flagMessage` take `reason` as OPTIONAL now.
  `FlagRequest.reason` is optional, so requiring it positionally was stricter than the
  endpoint. Widening only; existing calls still compile.

NOT removed, and why:

* `client.unbanUser` and `channel.unbanUser` keep their v1 implementation. The generated
  layer has NO unban endpoint — `ChatApi` exposes only the reads (`queryBannedUsers`,
  `queryFutureChannelBans`) and V2 moderation has `ban` with no matching `unban`, verified
  by sweeping every generated endpoint URL. Ban and unban must target the same system, so
  both stay reachable until the spec publishes one. `APIResponse` and `UnBanUserOptions`
  survive for the same reason.

  This leaves a temporary asymmetry: `channel.banUser` scopes through V2's `channel_cid`
  while `channel.unbanUser` still scopes through v1's `type` + `id`.

* `Moderation.unmuteUser` now calls the inherited `ModerationApi.unmute` instead of
  hand-posting to `/api/v2/moderation/unmute` — same URL, same body, same response shape.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
szuperaz and others added 10 commits August 20, 2026 14:07
Three sites cast the connected user with
`RequireLiteral<OwnUserResponse, 'blocked_user_ids'>` to satisfy a target typed
`UserResponse`, each carrying "TODO: drop RequireLiteral once the oapi spec is adjusted".

The spec needs no adjusting. Verified against the live API on both API versions:

* `OwnUserResponse` on the connect hello OMITS `blocked_user_ids` when nothing is blocked,
  and includes it once something is. Identical on v1 `/connect` (`health.check`) and v2
  `/api/v2/connect` (`connection.ok`).
* A plain `UserResponse` — another user embedded in a message or a member — ALWAYS carries
  it, as `[]` when empty.

So optional-on-own-user and required-on-`UserResponse` is exactly right, and following the
TODO would have made `OwnUserResponse` lie about the empty case.

The cast was also doing a second, unstated job: `client.user` is `ClientUser`
(`PartializeAllBut<OwnUserResponse, 'id'>`), so every field but `id` is optional there and
something has to lift it to the populated shape. That part is unavoidable and stays — but
it is now a plain `as UserResponse` that says what it means, rather than an indirection
through `RequireLiteral` plus a misleading TODO.

`blocked_user_ids` is supplied rather than asserted:

    user: { ...ownUser, blocked_user_ids: ownUser.blocked_user_ids ?? [] } as UserResponse

`?? []` is correct rather than defensive — absent genuinely means "nothing blocked", which
is how `client._handleClientEvent` already treats it when seeding `client.blockedUsers`.

BEHAVIOUR CHANGE:

* `blocked_user_ids: []` now appears on the `user` of offline-DB read rows and on
  `localMessage.user` when the connected user has nothing blocked. Previously the key was
  absent while the type claimed it was required. This aligns the runtime value with both
  the declared type and what the server sends for other users. Eight unit tests asserted
  the old shape and are updated.

BREAKING CHANGES:

* None. All three sites are internal; no exported type or signature changes.
  `stream-chat-react` and `stream-chat-react-native` both read the `client.blockedUsers`
  store rather than `blocked_user_ids` off a user object, so neither is affected.

`RequireLiteral` itself stays — its remaining users are the defensible ones, where the
narrowing is proven by a runtime check rather than asserted: `isOwnAnswer` in `poll.ts`,
`SharedLiveLocationResponse` (via `isValidLiveLocationMessage`), and `OGAttachment`.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
`LiveLocationManager.updateLiveLocation` carried a commented-out
`created_by_device_id: location.created_by_device_id` line under
"TODO: this is missing from the OAPI spec".

It is not missing — it is deliberately absent. `created_by_device_id` identifies the device
that opened the share and is fixed at creation; `UpdateLiveLocationRequest` has no device
field, so there is nothing to restore. Removing the dead line and the TODO that invited
someone to "fix" the spec.

The RULES header above it reached the right conclusion from the wrong premise — it said the
field "has currently no checks", implying the per-device intent was unenforced. The actual
reason any of a user's devices can push updates to one share is that the update payload
carries no device field at all. Reworded to say that.

`LocationComposer` still sets `created_by_device_id` when composing a NEW share, which is
correct — `SharedLocation` accepts it on create.

No behaviour change: the line was already commented out.

BREAKING CHANGES:

* None. Comment and dead-code only; no signature, type or runtime change.

Downstream: nothing to update in `stream-chat-react` or `stream-chat-react-native`. Both
call `channel.stopLiveLocationSharing(location)` with a full `SharedLocationResponseData`
rather than a trimmed request. That is type-legal (TypeScript does not excess-property-check
a variable) and runtime-safe, because the generated `ChatApi.updateLiveLocation` builds its
body from an explicit whitelist of `message_id` / `end_at` / `latitude` / `longitude` — so
`created_by_device_id` and the other response fields are dropped before the request is sent
and never reach the wire.

Noted while here, not changed: `channel.stopLiveLocationSharing` accepts a full
`UpdateLiveLocationRequest` but always overrides `end_at` with `new Date()`, so that field
is silently ignored. `Omit<UpdateLiveLocationRequest, 'end_at'>` would be the honest
signature.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
`MessagePaginationOptions` restated the generated `MessagePaginationParams`, and the two
lived side by side in `MessageIntervalPaginator` — which imported both and cast between them
(`messages: options as MessagePaginationParams`) to reach the request shape. The generated
`ChannelGetOrCreateRequest` already types `messages?: MessagePaginationParams`, so the
hand-written pair was a parallel vocabulary for something the spec defines.

Two divergences made them non-assignable, and both were wrong:

* `created_at_*` were widened to `string | Date`. The only place that exercised the string
  arm was `MessagePaginator.jumpToTheFirstUnreadMessage`, which called
  `lastReadAt.toISOString()` — converting a `Date` into a string purely to satisfy the
  SDK's own type, when the generated one wants the `Date`. The transport serializes dates,
  so passing it through is equivalent on the wire and one step shorter.

* `offset` was declared for message pagination, which the endpoint does not accept — the
  comment beside it even said "should be avoided with channel.query()". It was a silent
  no-op there, the same shape of bug as the `user_id` that `ChannelOptions` used to carry.

`PaginationOptions` is deleted rather than derived. It was never equivalent to the generated
`PaginationParams` (which is only `limit` / `offset`), and after the swap its sole consumer
was `linearPaginationFlags`, where it bounded one helper and named the query keys that imply
a cursor direction. That is cursor-derivation domain knowledge, not a request shape, so it
now lives beside the helper as a non-exported `LinearPaginationQueryShape`. `offset` stays
in that local shape because `PinnedMessagePaginationOptions` has one — `getPinnedMessages`
genuinely accepts it — and `TAILWARD_QUERY_PROPERTIES` lists it.

BREAKING CHANGES:

* `MessagePaginationOptions` is removed. Use the generated `MessagePaginationParams`. It is
  the same field set with two differences: `created_at_after`, `created_at_after_or_equal`,
  `created_at_around`, `created_at_before` and `created_at_before_or_equal` are `Date`
  rather than `string | Date`, and there is no `offset`. Pass a `Date` where you passed an
  ISO string; drop `offset`, which was never sent.

* `PaginationOptions` is removed with no direct replacement. For message pagination use
  `MessagePaginationParams`; for the `members` / `watchers` sub-objects of
  `ChannelGetOrCreateRequest` use the generated `PaginationParams` (`limit` / `offset`).

Neither type is referenced by `stream-chat-react` or `stream-chat-react-native`, so no
downstream change is required.

BEHAVIOUR CHANGE:

* `jumpToTheFirstUnreadMessage` now sends `created_at_around` as a `Date` rather than a
  pre-stringified ISO value. Identical on the wire; one unit test asserted the string form
  and is updated.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
`src/permissions.ts` held the deprecated v1 permission surface. Its own header said to stop
using it — "deprecated permission object class, you should use the new permission system v2
and use permissions defined in BuiltinPermissions to configure your channel types" — and
permissions are channel-type CONFIGURATION, which is server-side and left this package with
the rest of that surface (`Policy`, `PolicyRequest`, `BlockList`, the push-provider configs).

Nothing in `src` used any of it except `RoleName`, whose only consumer was
`channel.assignRoles`. Assigning channel roles is server-side too, so that goes as well and
takes `RoleName` with it. Neither `stream-chat-react` nor `stream-chat-react-native`
references a single symbol from the module.

BREAKING CHANGES:

* `src/permissions.ts` is removed entirely, so `export * from './permissions'` is gone from
  the public surface. Removed with no replacement in this SDK: `PermissionObject`, the
  `Permission` class, `AllowAll`, `DenyAll`, `Allow`, `Deny`, `AnyResource`, `AnyRole`,
  `MaxPriority`, `MinPriority`, `BuiltinRoles`, `BuiltinPermissions`, `RoleName`.
  Configure permissions with `@stream-io/node-sdk` or the dashboard.

* `Permission`, `AllowAll` and `DenyAll` were runtime values, not only types, so
  `import { Permission } from 'stream-chat'` now fails at runtime and not just at compile
  time — the same caveat as the `Product` enum.

* `channel.assignRoles(roles, message?, options?, requestOptions?)` is removed. Role
  assignment is server-side; use `@stream-io/node-sdk`.

Latent bug removed along with it, worth recording in case anyone copied the values:
`BuiltinPermissions` had six corrupted entries. An over-broad `Message` -> `MessageRequest`
rename in 62f0507 — a paginator commit, unrelated to permissions — rewrote the string
VALUES as well as type names, so `CreateMessage` read 'Create MessageRequest',
`RunMessageAction` read 'Run MessageRequest Action', and likewise for `DeleteAnyMessage`,
`DeleteOwnMessage`, `UpdateAnyMessage` and `UpdateOwnMessage`. Those are server-side
permission names the API matches on, so the constants emitted strings the backend does not
recognise. `62f05078` is not on `origin/master`, so this never shipped. The corruption was
confined to this file — `channel.ts`'s `'sendMessageRequestFn'` and friends are genuine
property names introduced by that same commit.

Note: `test/typescript/unit-test.ts` still imports `Permission`, `PermissionObject`,
`Allow`, `Deny`, `AnyResource`, `AnyRole` and `MaxPriority`. That harness is already broken
independently and is neither typechecked (`tsconfig.json` includes only `./src/**/*`) nor
run by `yarn test` (vitest covers `test/unit/**`), so it stays out of scope here.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
`VotingVisibility` was a hand-written enum whose two members duplicated
`CreatePollRequest['voting_visibility']` (`'anonymous' | 'public'`). It is now
derived from that request field, so a spec change flows through it.

BREAKING CHANGE: `VotingVisibility` is a type, not an enum, so it is no longer a
runtime value — `VotingVisibility.anonymous` must become `'anonymous'`. Member
names and string values were identical, so the rewrite is mechanical. Type-position
uses are unaffected, including `as VotingVisibility` casts when reading a poll:
`PollResponseData.voting_visibility` is typed `string` in the spec, so the
narrowing is still required.

`PollOptionData` (`UpdatePollOptionRequest & { position?: number }`) typed the
parameter of both `poll.createOption()` and `poll.updateOption()`, and described
neither:

- `position` appears nowhere in the OpenAPI spec, and the generated methods
  whitelist their body fields (`createPollOption` sends `text`/`custom`;
  `updatePollOption` sends `id`/`text`/`custom`), so it was dropped before the
  request left the client. Removing it changes no behaviour.
- `UpdatePollOptionRequest.id` is required, so `createOption()` demanded an id the
  create endpoint does not send. Callers cast around it — stream-chat-react-native
  did exactly that: `poll.createOption({ text } as PollOptionData)`.

BREAKING CHANGE: `PollOptionData` is removed. `poll.createOption()` now takes
`CreatePollOptionRequest` and `poll.updateOption()` takes `UpdatePollOptionRequest`.
Drop any `position` you were passing; it was never sent.

`PartialPollUpdate` is kept — it invents nothing and is already derived from
`UpdatePollRequest`, so it needs no hand edit on regeneration.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
`ModerationFlagOptions` types the `options` parameter of `moderation.flagUser()`
and `moderation.flagMessage()`. It was hand-written and disagreed with
`FlagRequest` in both directions:

- It carried `user_id`, which `FlagRequest` does not have. The generated `flag()`
  whitelists its body explicitly (`entity_id`, `entity_type`, `entity_creator_id`,
  `reason`, `custom`, `moderation_payload`), so a `user_id` passed here was
  discarded before the request was built. It never reached the server, and does
  not need to: the transport already sends `user_id` as a query parameter on every
  request, taken from `client.userId`.
- It omitted `entity_creator_id`, which the endpoint accepts. Both wrappers pinned
  it to `''` with no way to override; `options` is spread last, so it can now be
  supplied by the caller.

`reason` stays excluded: both wrappers take it positionally, and since `options`
is spread last, including it would let `options.reason` silently override the
positional argument.

BREAKING CHANGE: `ModerationFlagOptions.user_id` is removed. It was never
serialized into the flag request, so no call behaviour changes.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
The note left the empty-string default as an open question. Verified against the
live API instead: flagging a message and a user each way (`''`, omitted, the real
id) produced review-queue items whose `entity_creator_id` and resolved
`entity_creator` were the correct author in every case. The server derives the
creator from the entity and discards the empty string, so the default is harmless
and stays.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
`flagUser` / `flagMessage` pinned `entity_creator_id: ''` unconditionally, and an
empty string is serialized (`JSON.stringify` drops `undefined`, not `''`), so every
flag carried `"entity_creator_id": ""`. The field is optional in the spec, so there
was no reason to send it.

Verified against the live API rather than reasoned about: flagging a message and a
user each way — `''`, omitted, and the real creator id — produced review-queue
items whose `entity_creator_id` and resolved `entity_creator` were the correct
author in every case. The server derives the creator from the entity and discards
the empty string, so removing the default is behaviour-preserving. Attribution was
never broken; the field was dead weight on the wire.

Callers who do want to set it still can — `entity_creator_id` is part of
`ModerationFlagOptions` and `options` is spread last.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
@szuperaz szuperaz changed the title feat: reduce hand-written types and API wrappers feat!: reduce hand-written types and API wrappers Aug 21, 2026
@szuperaz szuperaz changed the title feat!: reduce hand-written types and API wrappers feat!: remove hand-written types and API wrappers Aug 21, 2026
@szuperaz
szuperaz marked this pull request as ready for review August 21, 2026 02:43
},
requestOptions?: StreamRequestOptions,
): Promise<StreamResponse<GetPinnedMessagesResponse>> {
if (!this.id) {

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Is it possible that id would be undefined? Can we create a Channel instance without an id?

}

getPinnedMessages(
request?: {

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Would it make sense to create a type for this request object?

@isekovanic
isekovanic merged commit dc49a78 into release-v10 Aug 21, 2026
4 checks passed
@isekovanic
isekovanic deleted the reduce-hand-written-types branch August 21, 2026 14:36
github-actions Bot pushed a commit that referenced this pull request Aug 21, 2026
## [10.0.0-rc.6](v10.0.0-rc.5...v10.0.0-rc.6) (2026-08-21)

### ⚠ BREAKING CHANGES

* remove hand-written types and API wrappers (#1836)
* **i18n:** `Streami18n.init()` now rejects when i18next fails to
initialize, where it previously logged and resolved. Callers that ignore
the returned promise will see an unhandled rejection. The instance
remains usable in its degraded English form either way.
`Streami18n.brand` is removed; nothing in either UI SDK read it, and a
custom integration that recognized instances through it should compare
against its own marker or accept the instance it was handed.

### Bug Fixes

* **i18n:** reject from init() and drop the Streami18n brand static ([#1839](#1839)) ([67bb1c7](67bb1c7))

### Features

* remove hand-written types and API wrappers ([#1836](#1836)) ([dc49a78](dc49a78))
@stream-ci-bot

Copy link
Copy Markdown

🎉 This PR is included in version 10.0.0-rc.6 🎉

The release is available on:

Your semantic-release bot 📦🚀

isekovanic pushed a commit to GetStream/stream-chat-react-native that referenced this pull request Aug 21, 2026
Not yet ready to be merged

Relevant stream-chat-js PR:
GetStream/stream-chat-js#1836

## 🎯 Goal

<!-- Describe why we are making this change -->

## 🛠 Implementation details

<!-- Provide a description of the implementation -->

## 🎨 UI Changes

<!-- Add relevant screenshots -->

<details>
<summary>iOS</summary>


<table>
    <thead>
        <tr>
            <td>Before</td>
            <td>After</td>
        </tr>
    </thead>
    <tbody>
        <tr>
            <td>
                <!--<img src="" /> -->
            </td>
            <td>
                <!--<img src="" /> -->
            </td>
        </tr>
    </tbody>
</table>
</details>


<details>
<summary>Android</summary>

<table>
    <thead>
        <tr>
            <td>Before</td>
            <td>After</td>
        </tr>
    </thead>
    <tbody>
        <tr>
            <td>
                <!--<img src="" /> -->
            </td>
            <td>
                <!--<img src="" /> -->
            </td>
        </tr>
    </tbody>
</table>
</details>

## 🧪 Testing

<!-- Explain how this change can be tested (or why it can't be tested)
-->

## ☑️ Checklist

- [ ] I have signed the [Stream
CLA](https://docs.google.com/forms/d/e/1FAIpQLScFKsKkAJI7mhCr7K9rEIOpqIDThrWxuvxnwUq2XkHyG154vQ/viewform)
(required)
- [ ] PR targets the `develop` branch
- [ ] Documentation is updated
- [ ] New code is tested in main example apps, including all possible
scenarios
  - [ ] SampleApp iOS and Android
  - [ ] Expo iOS and Android

---------

Co-authored-by: Claude Opus 5 <noreply@anthropic.com>
szuperaz added a commit to GetStream/stream-chat-react that referenced this pull request Aug 21, 2026
Not yet ready to be merged

Relevant stream-chat-js PR:
GetStream/stream-chat-js#1836

### 🎯 Goal

_Describe why we are making this change_

### 🛠 Implementation details

_Provide a description of the implementation_

### 🎨 UI Changes

_Add relevant screenshots_

---------

Co-authored-by: Claude Opus 5 <noreply@anthropic.com>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Projects

None yet

Development

Successfully merging this pull request may close these issues.

5 participants