Skip to content

feat: reduce hand-written types and API wrappers - #1836

Draft
szuperaz wants to merge 6 commits into
release-v10from
reduce-hand-written-types
Draft

feat: reduce hand-written types and API wrappers#1836
szuperaz wants to merge 6 commits into
release-v10from
reduce-hand-written-types

Conversation

@szuperaz

Copy link
Copy Markdown
Contributor

Not yet ready to be merged

CLA

  • I have signed the Stream CLA (required).
  • Code changes are tested

Description of the changes, What, Why and How?

Changelog

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
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant