You signed in with another tab or window. Reload to refresh your session.You signed out in another tab or window. Reload to refresh your session.You switched accounts on another tab or window. Reload to refresh your session.Dismiss alert
Copy file name to clipboardExpand all lines: blog/2025-02-14-typescript-sdk-release.md
+63-44Lines changed: 63 additions & 44 deletions
Display the source diff
Display the rich diff
Original file line number
Diff line number
Diff line change
@@ -43,7 +43,9 @@ Systems consuming Bluesky posts need to be able to determine what type of embed
43
43
"createdAt":"2021-09-01T12:34:56Z",
44
44
"embed": {
45
45
"$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
+
}
47
49
}
48
50
}
49
51
```
@@ -62,7 +64,9 @@ Since `embed` is an open union, it can be used to store anything. For example, a
62
64
}
63
65
```
64
66
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
+
:::
66
70
67
71
## Revamped TypeScript interfaces
68
72
@@ -85,23 +89,21 @@ Because the `$type` property is missing from that interface, developers could wr
85
89
```typescript
86
90
import { AppBskyFeedPost } from'@atproto/api'
87
91
88
-
// Aliased for clarity
89
-
typeBlueskyPost=AppBskyFeedPost.Main
90
-
91
-
// Invalid post, but TypeScript did not complain
92
-
const myPost:BlueskyPost= {
92
+
const myPost:AppBskyFeedPost.Main= {
93
93
text: 'Hey, check this out!',
94
94
createdAt: '2021-09-01T12:34:56Z',
95
95
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
+
97
99
video: {
98
100
/* reference to the video file, omitted for brevity */
99
101
},
100
102
},
101
103
}
102
104
```
103
105
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:
105
107
106
108
```typescript
107
109
exportinterfaceRecord {
@@ -121,15 +123,15 @@ It was possible to create a post with a completely invalid "video" embed, and st
121
123
```typescript
122
124
import { AppBskyFeedPost } from'@atproto/api'
123
125
124
-
// Aliased for clarity
125
-
typeBlueskyPost=AppBskyFeedPost.Main
126
-
127
-
const myPost:BlueskyPost= {
126
+
const myPost:AppBskyFeedPost.Main= {
128
127
text: 'Hey, check this out!',
129
128
createdAt: '2021-09-01T12:34:56Z',
129
+
130
+
// This is an invalid ember, but TypeScript
131
+
// does not complain.
130
132
embed: {
131
133
$type: 'app.bsky.embed.video',
132
-
video: 43,// This is invalid, but TypeScript does not complain
134
+
video: 43,
133
135
},
134
136
}
135
137
```
@@ -194,15 +196,16 @@ export interface Record {
194
196
}
195
197
```
196
198
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:
198
200
199
201
```tsx
200
202
import { AppBskyFeedPost } from'@atproto/api'
201
203
202
204
// Aliased for clarity
203
205
typeBlueskyPost=AppBskyFeedPost.Main
204
206
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)
206
209
declareconst post:BlueskyPost
207
210
208
211
// And we want to know what kind of embed it contains
@@ -217,12 +220,7 @@ if (embed?.$type === 'app.bsky.embed.images') {
217
220
218
221
### `is*` utility methods
219
222
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:
226
224
227
225
```typescript
228
226
exportinterfaceMain {
@@ -241,14 +239,16 @@ export function isMain(value: unknown): values is Main {
241
239
}
242
240
```
243
241
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:
245
248
246
249
```typescript
247
250
import { AppBskyEmbedImages } from'@atproto/api'
248
251
249
-
// Alias, for clarity
250
-
const isImages =AppBskyEmbedImages.isMain
251
-
252
252
// Get an invalid embed somehow
253
253
const invalidEmbed = {
254
254
$type: 'app.bsky.embed.images',
@@ -257,16 +257,17 @@ const invalidEmbed = {
257
257
258
258
// This predicate function only checks the value of
259
259
// 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
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:
268
269
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.
270
271
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.
271
272
272
273
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
282
283
```typescript
283
284
import { AppBskyEmbedImages } from'@atproto/api'
284
285
285
-
// Aliased for clarity
286
-
const isImages =AppBskyEmbedImages.isMain
287
-
288
286
// Get a post from the API (the API's contract
289
287
// guarantees the validity of the data)
290
-
declareconst post:BlueskyPost
288
+
declareconst post:AppBskyEmbedImages.isMain
291
289
292
290
// The `is*` utilities are an efficient way to
293
291
// discriminate **valid** data based on their `$type`
@@ -344,7 +342,7 @@ if (result.success) {
344
342
345
343
## Removal of the `[x: string]` index signature
346
344
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:
348
346
349
347
```typescript
350
348
exportinterfaceMain {
@@ -361,10 +359,7 @@ This signature allowed for undetectable mistakes to be performed:
361
359
```typescript
362
360
import { AppBskyEmbedVideo } from'@atproto/api'
363
361
364
-
// Aliased for clarity
365
-
const Video =AppBskyEmbedVideo.Main
366
-
367
-
const embed:Video= {
362
+
const embed:AppBskyEmbedVideo.Main= {
368
363
$type: 'app.bsky.embed.video',
369
364
video: { /* omitted */ }
370
365
// 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
377
372
```typescript
378
373
import { AppBskyEmbedVideo } from'@atproto/api'
379
374
380
-
// Aliased for clarity
381
-
const Video =AppBskyEmbedVideo.Main
382
-
383
-
const embed:Video= {
375
+
const embed:AppBskyEmbedVideo.Main= {
384
376
$type: 'app.bsky.embed.video',
385
377
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
+
386
386
// @ts-expect-error - custom field
387
387
comExampleCustomProp: 'custom value',
388
388
}
389
389
```
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.
// 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