Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Not possible to narrow records down when wanting to listen to everything #7

Open
Jiralite opened this issue Jan 11, 2025 · 4 comments

Comments

@Jiralite
Copy link

Jiralite commented Jan 11, 2025

Description

It does not seem possible to narrow down the union of records when wanting to listen to everything:

import { CommitType, EventType, Jetstream } from "@skyware/jetstream";

const jetstream = new Jetstream();

jetstream.on(EventType.Commit, (event) => {
	const commit = event.commit;

	if (commit.operation === CommitType.Create) {
		const record = commit.record;

		if (record.$type === "app.bsky.feed.post") {
			record.text; // Property 'text' does not exist...
		}
	}
});

jetstream.start();

I would have expected record to be of type AppBskyFeedPost.Record, but it was still AppBskyActorProfile.Record | AppBskyFeedGenerator.Record | AppBskyFeedLike.Record | AppBskyFeedPost.Record | AppBskyFeedPostgate.Record | AppBskyFeedRepost.Record | AppBskyFeedThreadgate.Record | AppBskyGraphBlock.Record | AppBskyGraphFollow.Record | AppBskyGraphList.Record | AppBskyGraphListblock.Record | AppBskyGraphListitem.Record | AppBskyGraphStarterpack.Record | AppBskyLabelerService.Record | ChatBskyActorDeclaration.Record.

tsconfig.json:

{
	"compilerOptions": {
		"strict": true,
		"module": "NodeNext",
		"moduleResolution": "NodeNext",
		"lib": ["ESNext"],
		"target": "ESNext",
	}
}

For now, just doing "text" in record suffices, I guess... unless I am being oblivious to something? If so, please let me know!

As a side note, I must skip the library check when type checking because it gives compile errors from partysocket. It's not strictly relevant to this package I guess.
index.ts:12:11 - error TS2339: Property 'text' does not exist on type 'AppBskyActorProfile.Record | AppBskyFeedGenerator.Record | AppBskyFeedLike.Record | AppBskyFeedPost.Record | AppBskyFeedPostgate.Record | AppBskyFeedRepost.Record | AppBskyFeedThreadgate.Record | AppBskyGraphBlock.Record | AppBskyGraphFollow.Record | AppBskyGraphList.Record | AppBskyGraphListblock.Record | ... 4 mor...'.
  Property 'text' does not exist on type 'Record'.

12    record.text;
             ~~~~

node_modules/.pnpm/[email protected]/node_modules/partysocket/ws.d.ts:5:189 - error TS2304: Cannot find name 'AddEventListenerOptions'.

5     addEventListener<K extends keyof EventMap>(type: K, callback: (event: EventMap[K] extends Event ? EventMap[K] : never) => EventMap[K] extends Event ? void : never, options?: boolean | AddEventListenerOptions): void;
                                                                                                                                                                                              ~~~~~~~~~~~~~~~~~~~~~~~

node_modules/.pnpm/[email protected]/node_modules/partysocket/ws.d.ts:6:46 - error TS2304: Cannot find name 'EventListenerOrEventListenerObject'.

6     addEventListener(type: string, callback: EventListenerOrEventListenerObject | null, options?: EventListenerOptions | boolean): void;
                                               ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~

node_modules/.pnpm/[email protected]/node_modules/partysocket/ws.d.ts:6:99 - error TS2304: Cannot find name 'EventListenerOptions'.

6     addEventListener(type: string, callback: EventListenerOrEventListenerObject | null, options?: EventListenerOptions | boolean): void;
                                                                                                    ~~~~~~~~~~~~~~~~~~~~

node_modules/.pnpm/[email protected]/node_modules/partysocket/ws.d.ts:7:192 - error TS2304: Cannot find name 'AddEventListenerOptions'.

7     removeEventListener<K extends keyof EventMap>(type: K, callback: (event: EventMap[K] extends Event ? EventMap[K] : never) => EventMap[K] extends Event ? void : never, options?: boolean | AddEventListenerOptions): void;
                                                                                                                                                                                                 ~~~~~~~~~~~~~~~~~~~~~~~

node_modules/.pnpm/[email protected]/node_modules/partysocket/ws.d.ts:8:49 - error TS2304: Cannot find name 'EventListenerOrEventListenerObject'.

8     removeEventListener(type: string, callback: EventListenerOrEventListenerObject | null, options?: EventListenerOptions | boolean): void;
                                                  ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~

node_modules/.pnpm/[email protected]/node_modules/partysocket/ws.d.ts:8:102 - error TS2304: Cannot find name 'EventListenerOptions'.

8     removeEventListener(type: string, callback: EventListenerOrEventListenerObject | null, options?: EventListenerOptions | boolean): void;
                                                                                                       ~~~~~~~~~~~~~~~~~~~~

node_modules/.pnpm/[email protected]/node_modules/partysocket/ws.d.ts:75:23 - error TS2304: Cannot find name 'BinaryType'.

75     get binaryType(): BinaryType;
                         ~~~~~~~~~~

node_modules/.pnpm/[email protected]/node_modules/partysocket/ws.d.ts:76:27 - error TS2304: Cannot find name 'BinaryType'.

76     set binaryType(value: BinaryType);
                             ~~~~~~~~~~


Found 9 errors in 2 files.

Errors  Files
     1  index.ts:12
     8  node_modules/.pnpm/[email protected]/node_modules/partysocket/ws.d.ts:5

Versions

  • Node.js 22.12.0
  • @skyware/jetstream 0.2.2
  • @types/node 22.10.5
  • typescript 5.7.3
@Jiralite Jiralite changed the title Not possible to narrow records to a specific union Not possible to narrow records down when wanting to listen to everything Jan 12, 2025
@futurGH
Copy link
Collaborator

futurGH commented Jan 24, 2025

This is difficult unfortunately — it's possible to receive records with an unknown $type and basically any shape, so the types need to account for that. It's effectively a version of microsoft/TypeScript#26277.

You can somewhat work around it with a type guard like this:

type ExtractByType<Record extends { $type: string }, Type extends string> = 
    Record extends Record
        ? string extends Record["$type"] ? never : Record & { $type: Type }
        : never;

function is<Type extends string, T extends { $type: string }>(record: T, type: Type): record is ExtractByType<T, Type> {
    return record.$type === type;
}

if (is(record, "app.bsky.feed.post")) {
	record.text;
}

I'm not sure I'll add this into the library, so I'll leave this issue up for other users to find.

@estrattonbailey
Copy link

Another option would be to pull the the atproto lexicon utils:

import {AppBskyFeedPost} from '@atproto/lexicon'

if (AppBskyFeedPost.isRecord(record)) {
  record.text; // => type 'string'
}

But that only checks the $type value. If you want to actually validate the data (using Zod):

if (AppBskyFeedPost.validateRecord(record).success) {
  record.text; // => type 'string'
}

The latter is more of a perf hit though, so avoid it in critical paths. If you're reading data from our APIs or firehose, you can trust the more simple and fast isRecord checks.

@futurGH
Copy link
Collaborator

futurGH commented Jan 24, 2025

Only problem is that this library uses Mary's atcute, not @atproto/api :) Mainly because the latter didn't have a type-level mapping from lexicon name to record type until Matthieu added it fairly recently iirc?

@estrattonbailey
Copy link

estrattonbailey commented Jan 24, 2025

Oh my mistake! Sorry about that 😅

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

No branches or pull requests

3 participants