@@ -110,28 +110,40 @@ To ingest an ad-hoc channel (e.g. navigating to a DM or search result) into the
110110
111111## Dates on response types are unix-nanosecond numbers
112112
113- ` stream-chat ` now types every ** server-sent** date as the unix-nanosecond ` number ` the API puts on the
113+ ` stream-chat ` now types every ** server-sent** date as the unix-nanosecond number the API puts on the
114114wire — ` created_at ` , ` updated_at ` , ` last_read ` , and every sibling on a response or event. It is not a
115115` Date ` and not an ISO string, and the React types that carry those values through changed with it.
116116
117- Two failure modes, neither of which is a type error:
117+ The type is ** ` TimestampNS ` ** , a branded ` number ` . Reading, comparing, sorting and subtracting work as
118+ with any number. Two things change at compile time:
118119
119- - ** Every ` Date ` -based path is out of range.** ` Date ` tops out near 8.64e15 ms while a current
120- timestamp is ~ 1.79e18, and a date library reads a bare number as ** milliseconds** — so both land on
121- an invalid instance rather than on a plausible wrong date. ` .toISOString() ` throws
122- ` RangeError: Invalid time value ` , usually mid-render; ` dayjs(created_at).format() ` instead returns
123- the literal string ` Invalid Date ` and renders it on screen.
124- - ** A unit mix-up between two ` number ` s is the silent one.** Comparing a wire timestamp against
125- ` Date.now() ` , or adding a millisecond duration to one, produces a plausible-looking number and no
126- complaint at all — see ` headerPosition ` below for a case with no type change to warn you.
120+ - ** ` new Date(timestamp) ` is a type error.** ` stream-chat ` 's published types augment the global
121+ ` DateConstructor ` , because a nanosecond value is out of ` Date ` 's range (` Date ` tops out near 8.64e15
122+ ms; a current timestamp is ~ 1.79e18) and yields an Invalid Date whose ` .toISOString() ` throws.
123+ - ** Minting one needs a helper.** A plain ` number ` is not assignable to a ` TimestampNS ` field or prop:
124+ use ` nowNs() ` , ` msToNs(ms) ` , ` dateToNs(date) ` , or ` asTimestampNS(n) ` for a value that is already in
125+ nanoseconds (a fixture, a stored value, the epoch ` asTimestampNS(0) ` ). Arithmetic drops the brand —
126+ wrap the result in ` asTimestampNS ` when it goes back into a timestamp.
127+
128+ What the compiler still does ** not** catch:
129+
130+ - ** Date libraries.** A date library reads a bare number as ** milliseconds** , so
131+ ` dayjs(created_at).format() ` returns the literal string ` Invalid Date ` and renders it on screen.
132+ - ** Fallbacks and derived values.** ` new Date(ts ?? Date.now()) ` and ` new Date(Math.max(a, b)) `
133+ compile, because the argument is no longer purely ` TimestampNS ` . Convert first, then fall back.
134+ - ** A unit mix-up between two numbers.** Comparing a wire timestamp against ` Date.now() ` , or adding a
135+ millisecond duration to one, produces a plausible-looking number and no complaint at all.
127136
128137### The public React types that changed
129138
130- | Type | v14 | v15 |
131- | ----------------------------------------------------- | ----------------------------- | ------------------------------- |
132- | ` ChatContextValue.latestMessageDatesByChannels ` | ` Record<ChannelConfId, Date> ` | ` Record<ChannelConfId, number> ` |
133- | ` ProcessMessagesParams.lastRead ` (` processMessages ` ) | ` Date \| null ` | ` number \| null ` |
134- | ` VirtualizedMessageList ` render props: ` lastReadDate ` | ` Date \| null ` | ` number \| null ` |
139+ | Type | v14 | v15 |
140+ | ----------------------------------------------------- | ------------------- | ----------------------- |
141+ | ` ProcessMessagesParams.lastRead ` (` processMessages ` ) | ` Date \| null ` | ` TimestampNS \| null ` |
142+ | ` VirtualizedMessageList ` render props: ` lastReadDate ` | ` Date \| null ` | ` TimestampNS \| null ` |
143+ | ` MessageList ` ` headerPosition ` / ` insertIntro ` | ` number ` (epoch ms) | ` TimestampNS ` (unix ns) |
144+
145+ ` ChatContextValue.latestMessageDatesByChannels ` is not in this table because it is ** removed** , not
146+ retyped — see [ below] ( #chatcontextlatestmessagedatesbychannels--removed ) .
135147
136148` DateSeparatorMessage ` (a member of the exported ` RenderedMessage ` union) changed shape rather than
137149type: it ** lost its ` type: MessageLabel ` field** , and ` unread ` is now optional. The ` type ` field was
@@ -144,10 +156,10 @@ Comparisons get simpler, not harder — compare and sort the raw numbers and dro
144156
145157``` ts
146158// v14
147- if (latestMessageDatesByChannels [ cid ]. getTime () < new Date (message .created_at ).getTime ()) { … }
159+ if (new Date ( a . created_at ). getTime () < new Date (b .created_at ).getTime ()) { … }
148160
149161// v15
150- if (latestMessageDatesByChannels [ cid ] < message .created_at ) { … }
162+ if (a . created_at < b .created_at ) { … }
151163```
152164
153165### Presentational props still take ` Date `
@@ -178,16 +190,17 @@ const createdAt = convertTimestampToDate(message.created_at);
178190< DateSeparator date = {convertTimestampToDate(message.created_at) ?? new Date()} / >
179191```
180192
181- ` nsToDate ` / ` dateToNs ` / ` nsToMs ` / ` msToNs ` / ` nowNs ` are exported alongside it for values known to be
182- present. Note that ** outgoing request** date fields are still ` Date ` (filter bounds like
193+ ` nsToDate ` / ` dateToNs ` / ` nsToMs ` / ` msToNs ` / ` nowNs ` / ` asTimestampNS ` are exported alongside it
194+ for values known to be present. Note that ** outgoing request** date fields are still ` Date ` (filter bounds like
183195` created_at_before ` , plus ` remind_at ` and ` message_timestamp ` ) — ` JSON.stringify ` emits RFC3339 for a
184196` Date ` , which is what the request spec declares. Use ` nsToDate ` when handing a server-sent timestamp
185197back to the API.
186198
187- ### ` MessageList ` 's ` headerPosition ` prop changed unit, not type
199+ ### ` MessageList ` 's ` headerPosition ` prop changed unit
188200
189201` headerPosition ` is compared against ` message.created_at ` , so it is now ** unix nanoseconds** — it was
190- epoch milliseconds while ` created_at ` was a ` Date ` . The type is still ` number ` , so nothing warns.
202+ epoch milliseconds while ` created_at ` was a ` Date ` . It is typed ` TimestampNS ` , so a millisecond
203+ ` number ` no longer compiles: pass ` message.created_at ` or ` msToNs(ms) ` .
191204
192205### Peer-dependency gate before release
193206
@@ -202,7 +215,9 @@ range to the version that exports them and verify from a clean install with no `
202215A fixture that hands the SDK a ` Date ` cannot catch either failure mode above, and will diverge from
203216runtime behavior. The SDK's own suite normalizes through
204217` mock-builders/generator/time.ts ` (` convertDateToTimestamp ` ), which accepts a ` Date ` , an ISO string or a
205- raw wire number so tests stay readable while the value on the wire stays a number.
218+ raw wire number so tests stay readable while the value on the wire stays a number. It returns
219+ ` TimestampNS ` , so a generated fixture is assignable to the response types; a hand-written literal
220+ (` created_at: 0 ` , ` now - msToNs(1000) ` ) needs ` asTimestampNS(...) ` .
206221
207222## i18n: English-only bundle, namespaced translation keys
208223
0 commit comments