Skip to content

Commit 19395be

Browse files
tidy
1 parent f580d50 commit 19395be

File tree

1 file changed

+63
-44
lines changed

1 file changed

+63
-44
lines changed

blog/2025-02-14-typescript-sdk-release.md

Lines changed: 63 additions & 44 deletions
Original file line numberDiff line numberDiff line change
@@ -43,7 +43,9 @@ Systems consuming Bluesky posts need to be able to determine what type of embed
4343
"createdAt": "2021-09-01T12:34:56Z",
4444
"embed": {
4545
"$type": "app.bsky.embed.video",
46-
"video": { /* reference to the video file, omitted for brevity */ }
46+
"video": {
47+
/* reference to the video file, omitted for brevity */
48+
}
4749
}
4850
}
4951
```
@@ -62,7 +64,9 @@ Since `embed` is an open union, it can be used to store anything. For example, a
6264
}
6365
```
6466

65-
> Note: Only systems that know about the `com.example.calendar.event` lexicon can interpret this data. The official Bluesky app will typically only know about the data types defined in the `app.bsky` lexicons.
67+
:::note
68+
Only systems that know about the `com.example.calendar.event` lexicon can interpret this data. The official Bluesky app will typically only know about the data types defined in the `app.bsky` lexicons.
69+
:::
6670

6771
## Revamped TypeScript interfaces
6872

@@ -85,23 +89,21 @@ Because the `$type` property is missing from that interface, developers could wr
8589
```typescript
8690
import { AppBskyFeedPost } from '@atproto/api'
8791

88-
// Aliased for clarity
89-
type BlueskyPost = AppBskyFeedPost.Main
90-
91-
// Invalid post, but TypeScript did not complain
92-
const myPost: BlueskyPost = {
92+
const myPost: AppBskyFeedPost.Main = {
9393
text: 'Hey, check this out!',
9494
createdAt: '2021-09-01T12:34:56Z',
9595
embed: {
96-
// Notice how we are missing the `$type` property here
96+
// Notice how we are missing the `$type` property
97+
// here. TypeScript did not complain about this.
98+
9799
video: {
98100
/* reference to the video file, omitted for brevity */
99101
},
100102
},
101103
}
102104
```
103105

104-
Similarly, because Bluesky post’s `embed` property was previously [typed](https://github.com/bluesky-social/atproto/blob/5ece8c6aeab9c5c3f51295d93ed6e27c3c6095c2/packages/api/src/client/types/app/bsky/feed/post.ts#L25-L31) like this:
106+
Similarly, because Bluesky post’s `embed` property was [previously](https://github.com/bluesky-social/atproto/blob/5ece8c6aeab9c5c3f51295d93ed6e27c3c6095c2/packages/api/src/client/types/app/bsky/feed/post.ts#L25-L31) typed like this:
105107

106108
```typescript
107109
export interface Record {
@@ -121,15 +123,15 @@ It was possible to create a post with a completely invalid "video" embed, and st
121123
```typescript
122124
import { AppBskyFeedPost } from '@atproto/api'
123125

124-
// Aliased for clarity
125-
type BlueskyPost = AppBskyFeedPost.Main
126-
127-
const myPost: BlueskyPost = {
126+
const myPost: AppBskyFeedPost.Main = {
128127
text: 'Hey, check this out!',
129128
createdAt: '2021-09-01T12:34:56Z',
129+
130+
// This is an invalid ember, but TypeScript
131+
// does not complain.
130132
embed: {
131133
$type: 'app.bsky.embed.video',
132-
video: 43, // This is invalid, but TypeScript does not complain
134+
video: 43,
133135
},
134136
}
135137
```
@@ -194,15 +196,16 @@ export interface Record {
194196
}
195197
```
196198

197-
In addition to preventing the _creation_ of invalid data as seen at the beginning of this section, this change also allows to properly discriminate types when _accessing_ the data. For example, one can now do:
199+
In addition to preventing the _creation_ of invalid data as seen before, this change also allows to properly discriminate types when _accessing_ the data. For example, one can now do:
198200

199201
```tsx
200202
import { AppBskyFeedPost } from '@atproto/api'
201203

202204
// Aliased for clarity
203205
type BlueskyPost = AppBskyFeedPost.Main
204206

205-
// Say we got some random post somehow (typically via an api call)
207+
// Say we got some random post somehow (typically
208+
// via an api call)
206209
declare const post: BlueskyPost
207210

208211
// And we want to know what kind of embed it contains
@@ -217,12 +220,7 @@ if (embed?.$type === 'app.bsky.embed.images') {
217220

218221
### `is*` utility methods
219222

220-
The example above shows how data can be discriminated based on the `$type` property. There are, however, several disadvantages to relying on string comparison for discriminating data types:
221-
222-
- Having to use inline strings yields a lot of code, hurting readability and bundle size.
223-
- In particular instances, the `$type` property can actually have two values to describe the same lexicon. An "images" embed, for example, can use both `app.bsky.embed.images` and `app.bsky.embed.images#main` as `$type`. This makes the previous point even worse.
224-
225-
In order to alleviate these issues, the SDK provides type checking predicate functions. In their previous implementation, the `is*` utilities were defined as follows:
223+
The example above shows how data can be discriminated based on the `$type` property. The SDK provides utility methods to perform this kind of discrimination. These methods are named `is*` and are generated from the lexicons. For example, the `app.bsky.embed.images` lexicon used to generate the following `isMain` utility method:
226224

227225
```typescript
228226
export interface Main {
@@ -241,14 +239,16 @@ export function isMain(value: unknown): values is Main {
241239
}
242240
```
243241

244-
As can be seen from the example implementation above, the predicate functions would cast any object containing the expected `$type` property into the corresponding type, without checking for the actual validity of other properties. This could yield runtime errors that could have been avoided during development:
242+
That implementation of the discriminator is invalid.
243+
244+
- Fist because a `$type` is not allowed to end with `#main` (as per atproto specification).
245+
- Second because the `isMain` function does not actually check the structure of the object, only its `$type` property.
246+
247+
This invalid behavior could yield runtime errors that could otherwise have been avoided during development:
245248

246249
```typescript
247250
import { AppBskyEmbedImages } from '@atproto/api'
248251

249-
// Alias, for clarity
250-
const isImages = AppBskyEmbedImages.isMain
251-
252252
// Get an invalid embed somehow
253253
const invalidEmbed = {
254254
$type: 'app.bsky.embed.images',
@@ -257,16 +257,17 @@ const invalidEmbed = {
257257

258258
// This predicate function only checks the value of
259259
// the `$type` property, making the condition "true" here
260-
if (isImages(invalidEmbed)) {
261-
// No TypeScript error, BUT causes a runtime
262-
// error because there is no "images" property !
260+
if (AppBskyEmbedImages.isMain(invalidEmbed)) {
261+
// However, the `images` property is missing here.
262+
// TypeScript does not complain about this, but the
263+
// following line will throw a runtime error:
263264
console.log('First image:', invalidEmbed.images[0])
264265
}
265266
```
266267

267-
The root of the issue here is that the `is*` utility methods perform type casting of objects solely based on the value of their `$type` property. There were basically two ways of fixing this issue:
268+
The root of the issue here is that the `is*` utility methods perform type casting of objects solely based on the value of their `$type` property. There were basically two ways we could fix this behavior:
268269

269-
1. Alter the implementation to actually validate the object's structure. This is a non-breaking change that has a negative impact on performance.
270+
1. Alter the implementation to actually validate the object's structure. This would be a non-breaking change that has a negative impact on performance.
270271
2. Alter the function signature to describe what the function actually does. This is a breaking change because TypeScript would start (rightfully) returning lots of errors in places where these functions are used.
271272

272273
Because this release introduces other breaking changes, and because adapting our own codebase to this change showed it made more sense, we decided to adopt the latter option.
@@ -282,12 +283,9 @@ This is the case for example when working with data obtained from the API. Becau
282283
```typescript
283284
import { AppBskyEmbedImages } from '@atproto/api'
284285

285-
// Aliased for clarity
286-
const isImages = AppBskyEmbedImages.isMain
287-
288286
// Get a post from the API (the API's contract
289287
// guarantees the validity of the data)
290-
declare const post: BlueskyPost
288+
declare const post: AppBskyEmbedImages.isMain
291289

292290
// The `is*` utilities are an efficient way to
293291
// discriminate **valid** data based on their `$type`
@@ -344,7 +342,7 @@ if (result.success) {
344342

345343
## Removal of the `[x: string]` index signature
346344

347-
Another property of Atproto being an "open protocol" is the fact that objects are allowed to contain additional, unspecified, properties (though this should be done with caution to avoid incompatibility with properties added in the future). This used to be represented in the type system using a `[k: string]: unknown` index signature in generated interfaces. This is how the video embed was represented:
345+
Another property of Atproto being an "open protocol" is the fact that objects are allowed to contain additional — unspecified — properties (though this should be done with caution to avoid incompatibility with properties added in the future). This used to be represented in the type system using a `[k: string]: unknown` index signature in generated interfaces. This is how the video embed used to be represented:
348346

349347
```typescript
350348
export interface Main {
@@ -361,10 +359,7 @@ This signature allowed for undetectable mistakes to be performed:
361359
```typescript
362360
import { AppBskyEmbedVideo } from '@atproto/api'
363361

364-
// Aliased for clarity
365-
const Video = AppBskyEmbedVideo.Main
366-
367-
const embed: Video = {
362+
const embed: AppBskyEmbedVideo.Main = {
368363
$type: 'app.bsky.embed.video',
369364
video: { /* omitted */ }
370365
// Notice the typo in `alt`, not resulting in a TypeScript error
@@ -377,13 +372,37 @@ We removed that signature, requiring any un-specified fields intentionally added
377372
```typescript
378373
import { AppBskyEmbedVideo } from '@atproto/api'
379374

380-
// Aliased for clarity
381-
const Video = AppBskyEmbedVideo.Main
382-
383-
const embed: Video = {
375+
const embed: AppBskyEmbedVideo.Main = {
384376
$type: 'app.bsky.embed.video',
385377
video: { /* omitted */ }
378+
379+
// Next line wil result in a TypeScript
380+
// error (a string is expected).
381+
alt: 123,
382+
383+
// Un-specified fields must now be explicitly
384+
// marked as such:
385+
386386
// @ts-expect-error - custom field
387387
comExampleCustomProp: 'custom value',
388388
}
389389
```
390+
391+
## New `asPredicate` function
392+
393+
The SDK exposes a new `asPredicate` function. This function allows to convert a `validate*` function into a predicate function. This can be useful when working with libraries that expect a predicate function to be passed as an argument.
394+
395+
```typescript
396+
import { AppBskyEmbedImages, asPredicate } from '@atproto/api'
397+
398+
const isValidImage = asPredicate(AppBskyEmbedImages.validateMain)
399+
400+
declare const someArray: unknown[]
401+
402+
// This will be typed as `AppBskyEmbedImages.Main[]`
403+
const images = someArray.filter(isValidImage)
404+
```
405+
406+
## Other considerations
407+
408+
When upgrading, please make sure that your project does not depend on multiple versions of the `@atproto/*` packages. Use [resolutions](https://classic.yarnpkg.com/en/docs/selective-version-resolutions/) or [overrides](https://docs.npmjs.com/cli/v9/configuring-npm/package-json#overrides) in your `package.json` to pin the dependencies to the same version.

0 commit comments

Comments
 (0)