···11+import { setup } from "@ark/attest";
22+33+// config options can be passed here
44+export default () =>
55+ setup({
66+ // Set to true during development to skip type checking (faster)
77+ skipTypes: false,
88+99+ // Fail if benchmarks deviate by more than 20%
1010+ benchPercentThreshold: 20,
1111+ });
+2
packages/prototypey/src/index.ts
···11+export * from "./lib.ts";
22+export * from "./infer.ts";
+141
packages/prototypey/src/infer.ts
···11+import { Prettify } from "./type-utils.ts";
22+33+/* eslint-disable @typescript-eslint/no-empty-object-type */
44+type InferType<T> = T extends { type: "record" }
55+ ? InferRecord<T>
66+ : T extends { type: "object" }
77+ ? InferObject<T>
88+ : T extends { type: "array" }
99+ ? InferArray<T>
1010+ : T extends { type: "params" }
1111+ ? InferParams<T>
1212+ : T extends { type: "union" }
1313+ ? InferUnion<T>
1414+ : T extends { type: "token" }
1515+ ? InferToken<T>
1616+ : T extends { type: "ref" }
1717+ ? InferRef<T>
1818+ : T extends { type: "unknown" }
1919+ ? unknown
2020+ : T extends { type: "null" }
2121+ ? null
2222+ : T extends { type: "boolean" }
2323+ ? boolean
2424+ : T extends { type: "integer" }
2525+ ? number
2626+ : T extends { type: "string" }
2727+ ? string
2828+ : T extends { type: "bytes" }
2929+ ? Uint8Array
3030+ : T extends { type: "cid-link" }
3131+ ? string
3232+ : T extends { type: "blob" }
3333+ ? Blob
3434+ : never;
3535+3636+type InferToken<T> = T extends { enum: readonly (infer U)[] } ? U : string;
3737+3838+export type GetRequired<T> = T extends { required: readonly (infer R)[] }
3939+ ? R
4040+ : never;
4141+export type GetNullable<T> = T extends { nullable: readonly (infer N)[] }
4242+ ? N
4343+ : never;
4444+4545+type InferObject<
4646+ T,
4747+ Nullable extends string = GetNullable<T> & string,
4848+ Required extends string = GetRequired<T> & string,
4949+ NullableAndRequired extends string = Required & Nullable & string,
5050+ Normal extends string = "properties" extends keyof T
5151+ ? Exclude<keyof T["properties"], Required | Nullable> & string
5252+ : never,
5353+> = Prettify<
5454+ T extends { properties: infer P }
5555+ ? {
5656+ -readonly [K in Normal]?: InferType<P[K & keyof P]>;
5757+ } & {
5858+ -readonly [K in Exclude<Required, NullableAndRequired>]-?: InferType<
5959+ P[K & keyof P]
6060+ >;
6161+ } & {
6262+ -readonly [K in Exclude<Nullable, NullableAndRequired>]?: InferType<
6363+ P[K & keyof P]
6464+ > | null;
6565+ } & {
6666+ -readonly [K in NullableAndRequired]: InferType<P[K & keyof P]> | null;
6767+ }
6868+ : {}
6969+>;
7070+7171+type InferArray<T> = T extends { items: infer Items }
7272+ ? InferType<Items>[]
7373+ : never[];
7474+7575+type InferUnion<T> = T extends { refs: readonly (infer R)[] }
7676+ ? R extends string
7777+ ? { $type: R; [key: string]: unknown }
7878+ : never
7979+ : never;
8080+8181+type InferRef<T> = T extends { ref: infer R }
8282+ ? R extends string
8383+ ? { $type: R; [key: string]: unknown }
8484+ : unknown
8585+ : unknown;
8686+8787+type InferParams<T> = InferObject<T>;
8888+8989+type InferRecord<T> = T extends { record: infer R }
9090+ ? R extends { type: "object" }
9191+ ? InferObject<R>
9292+ : R extends { type: "union" }
9393+ ? InferUnion<R>
9494+ : unknown
9595+ : unknown;
9696+9797+/**
9898+ * Recursively replaces stub references in a type with their actual definitions.
9999+ * Detects circular references and missing references, returning string literal error messages.
100100+ */
101101+type ReplaceRefsInType<T, Defs, Visited = never> =
102102+ // Check if this is a ref stub type (has $type starting with #)
103103+ T extends { $type: `#${infer DefName}` }
104104+ ? DefName extends keyof Defs
105105+ ? // Check for circular reference
106106+ DefName extends Visited
107107+ ? `[Circular reference detected: #${DefName}]`
108108+ : // Recursively resolve the ref and preserve the $type marker
109109+ Prettify<
110110+ ReplaceRefsInType<Defs[DefName], Defs, Visited | DefName> & {
111111+ $type: T["$type"];
112112+ }
113113+ >
114114+ : // Reference not found in definitions
115115+ `[Reference not found: #${DefName}]`
116116+ : // Handle arrays (but not Uint8Array or other typed arrays)
117117+ T extends Uint8Array | Blob
118118+ ? T
119119+ : T extends readonly (infer Item)[]
120120+ ? ReplaceRefsInType<Item, Defs, Visited>[]
121121+ : // Handle plain objects (exclude built-in types and functions)
122122+ T extends object
123123+ ? T extends (...args: unknown[]) => unknown
124124+ ? T
125125+ : { [K in keyof T]: ReplaceRefsInType<T[K], Defs, Visited> }
126126+ : // Primitives pass through unchanged
127127+ T;
128128+129129+/**
130130+ * Infers the TypeScript type for a lexicon namespace, returning only the 'main' definition
131131+ * with all local refs (#user, #post, etc.) resolved to their actual types.
132132+ */
133133+export type Infer<T extends { id: string; defs: Record<string, unknown> }> =
134134+ Prettify<
135135+ "main" extends keyof T["defs"]
136136+ ? { $type: T["id"] } & ReplaceRefsInType<
137137+ InferType<T["defs"]["main"]>,
138138+ { [K in keyof T["defs"]]: InferType<T["defs"][K]> }
139139+ >
140140+ : never
141141+ >;
+579
packages/prototypey/src/lib.ts
···11+/* eslint-disable @typescript-eslint/no-empty-object-type */
22+import type { Infer } from "./infer.ts";
33+import type { UnionToTuple } from "./type-utils.ts";
44+55+/** @see https://atproto.com/specs/lexicon#overview-of-types */
66+type LexiconType =
77+ // Concrete types
88+ | "null"
99+ | "boolean"
1010+ | "integer"
1111+ | "string"
1212+ | "bytes"
1313+ | "cid-link"
1414+ | "blob"
1515+ // Container types
1616+ | "array"
1717+ | "object"
1818+ | "params"
1919+ // Meta types
2020+ | "token"
2121+ | "ref"
2222+ | "union"
2323+ | "unknown"
2424+ // Primary types
2525+ | "record"
2626+ | "query"
2727+ | "procedure"
2828+ | "subscription";
2929+3030+/**
3131+ * Common options available for lexicon items.
3232+ * @see https://atproto.com/specs/lexicon#string-formats
3333+ */
3434+interface LexiconItemCommonOptions {
3535+ /** Indicates this field must be provided */
3636+ required?: boolean;
3737+ /** Indicates this field can be explicitly set to null */
3838+ nullable?: boolean;
3939+}
4040+4141+/**
4242+ * Base interface for all lexicon items.
4343+ * @see https://atproto.com/specs/lexicon#overview-of-types
4444+ */
4545+interface LexiconItem extends LexiconItemCommonOptions {
4646+ type: LexiconType;
4747+}
4848+4949+/**
5050+ * Definition in a lexicon namespace.
5151+ * @see https://atproto.com/specs/lexicon#lexicon-document
5252+ */
5353+interface Def {
5454+ type: LexiconType;
5555+}
5656+5757+/**
5858+ * Lexicon namespace document structure.
5959+ * @see https://atproto.com/specs/lexicon#lexicon-document
6060+ */
6161+interface LexiconNamespace {
6262+ /** Namespaced identifier (NSID) for this lexicon */
6363+ id: string;
6464+ /** Named definitions within this namespace */
6565+ defs: Record<string, Def>;
6666+}
6767+6868+/**
6969+ * String type options.
7070+ * @see https://atproto.com/specs/lexicon#string
7171+ */
7272+interface StringOptions extends LexiconItemCommonOptions {
7373+ /**
7474+ * Semantic string format constraint.
7575+ * @see https://atproto.com/specs/lexicon#string-formats
7676+ */
7777+ format?:
7878+ | "at-identifier" // Handle or DID
7979+ | "at-uri" // AT Protocol URI
8080+ | "cid" // Content Identifier
8181+ | "datetime" // Timestamp (UTC, ISO 8601)
8282+ | "did" // Decentralized Identifier
8383+ | "handle" // User handle identifier
8484+ | "nsid" // Namespaced Identifier
8585+ | "tid" // Timestamp Identifier
8686+ | "record-key" // Repository record key
8787+ | "uri" // Generic URI
8888+ | "language"; // IETF BCP 47 language tag
8989+ /** Maximum string length in bytes */
9090+ maxLength?: number;
9191+ /** Minimum string length in bytes */
9292+ minLength?: number;
9393+ /** Maximum string length in Unicode graphemes */
9494+ maxGraphemes?: number;
9595+ /** Minimum string length in Unicode graphemes */
9696+ minGraphemes?: number;
9797+ /** Hints at expected values, not enforced */
9898+ knownValues?: string[];
9999+ /** Restricts to an exact set of string values */
100100+ enum?: string[];
101101+ /** Default value if not provided */
102102+ default?: string;
103103+ /** Fixed, unchangeable value */
104104+ const?: string;
105105+}
106106+107107+/**
108108+ * Boolean type options.
109109+ * @see https://atproto.com/specs/lexicon#boolean
110110+ */
111111+interface BooleanOptions extends LexiconItemCommonOptions {
112112+ /** Default value if not provided */
113113+ default?: boolean;
114114+ /** Fixed, unchangeable value */
115115+ const?: boolean;
116116+}
117117+118118+/**
119119+ * Integer type options.
120120+ * @see https://atproto.com/specs/lexicon#integer
121121+ */
122122+interface IntegerOptions extends LexiconItemCommonOptions {
123123+ /** Minimum allowed value (inclusive) */
124124+ minimum?: number;
125125+ /** Maximum allowed value (inclusive) */
126126+ maximum?: number;
127127+ /** Restricts to an exact set of integer values */
128128+ enum?: number[];
129129+ /** Default value if not provided */
130130+ default?: number;
131131+ /** Fixed, unchangeable value */
132132+ const?: number;
133133+}
134134+135135+/**
136136+ * Bytes type options for arbitrary byte arrays.
137137+ * @see https://atproto.com/specs/lexicon#bytes
138138+ */
139139+interface BytesOptions extends LexiconItemCommonOptions {
140140+ /** Minimum byte array length */
141141+ minLength?: number;
142142+ /** Maximum byte array length */
143143+ maxLength?: number;
144144+}
145145+146146+/**
147147+ * Blob type options for binary data with MIME types.
148148+ * @see https://atproto.com/specs/lexicon#blob
149149+ */
150150+interface BlobOptions extends LexiconItemCommonOptions {
151151+ /** Allowed MIME types (e.g., ["image/png", "image/jpeg"]) */
152152+ accept?: string[];
153153+ /** Maximum blob size in bytes */
154154+ maxSize?: number;
155155+}
156156+157157+/**
158158+ * Array type options.
159159+ * @see https://atproto.com/specs/lexicon#array
160160+ */
161161+interface ArrayOptions extends LexiconItemCommonOptions {
162162+ /** Minimum array length */
163163+ minLength?: number;
164164+ /** Maximum array length */
165165+ maxLength?: number;
166166+}
167167+168168+/**
169169+ * Record type options for repository records.
170170+ * @see https://atproto.com/specs/lexicon#record
171171+ */
172172+interface RecordOptions {
173173+ /** Record key strategy: "self" for self-describing or "tid" for timestamp IDs */
174174+ key: "self" | "tid";
175175+ /** Object schema defining the record structure */
176176+ record: { type: "object" };
177177+ /** Human-readable description */
178178+ description?: string;
179179+}
180180+181181+/**
182182+ * Union type options for multiple possible types.
183183+ * @see https://atproto.com/specs/lexicon#union
184184+ */
185185+interface UnionOptions extends LexiconItemCommonOptions {
186186+ /** If true, only listed refs are allowed; if false, additional types may be added */
187187+ closed?: boolean;
188188+}
189189+190190+/**
191191+ * Map of property names to their lexicon item definitions.
192192+ * @see https://atproto.com/specs/lexicon#object
193193+ */
194194+type ObjectProperties = Record<
195195+ string,
196196+ {
197197+ type: LexiconType;
198198+ }
199199+>;
200200+201201+type RequiredKeys<T> = {
202202+ [K in keyof T]: T[K] extends { required: true } ? K : never;
203203+}[keyof T];
204204+205205+type NullableKeys<T> = {
206206+ [K in keyof T]: T[K] extends { nullable: true } ? K : never;
207207+}[keyof T];
208208+209209+/**
210210+ * Resulting object schema with required and nullable fields extracted.
211211+ * @see https://atproto.com/specs/lexicon#object
212212+ */
213213+type ObjectResult<T extends ObjectProperties> = {
214214+ type: "object";
215215+ /** Property definitions */
216216+ properties: {
217217+ [K in keyof T]: T[K] extends { type: "object" }
218218+ ? T[K]
219219+ : Omit<T[K], "required" | "nullable">;
220220+ };
221221+} & ([RequiredKeys<T>] extends [never]
222222+ ? {}
223223+ : { required: UnionToTuple<RequiredKeys<T>> }) &
224224+ ([NullableKeys<T>] extends [never]
225225+ ? {}
226226+ : { nullable: UnionToTuple<NullableKeys<T>> });
227227+228228+/**
229229+ * Map of parameter names to their lexicon item definitions.
230230+ * @see https://atproto.com/specs/lexicon#params
231231+ */
232232+type ParamsProperties = Record<string, LexiconItem>;
233233+234234+/**
235235+ * Resulting params schema with required fields extracted.
236236+ * @see https://atproto.com/specs/lexicon#params
237237+ */
238238+type ParamsResult<T extends ParamsProperties> = {
239239+ type: "params";
240240+ /** Parameter definitions */
241241+ properties: {
242242+ [K in keyof T]: Omit<T[K], "required" | "nullable">;
243243+ };
244244+} & ([RequiredKeys<T>] extends [never]
245245+ ? {}
246246+ : { required: UnionToTuple<RequiredKeys<T>> });
247247+248248+/**
249249+ * HTTP request or response body schema.
250250+ * @see https://atproto.com/specs/lexicon#http-endpoints
251251+ */
252252+interface BodySchema {
253253+ /** MIME type encoding (typically "application/json") */
254254+ encoding: "application/json" | (string & {});
255255+ /** Human-readable description */
256256+ description?: string;
257257+ /** Object schema defining the body structure */
258258+ schema?: ObjectResult<ObjectProperties>;
259259+}
260260+261261+/**
262262+ * Error definition for HTTP endpoints.
263263+ * @see https://atproto.com/specs/lexicon#http-endpoints
264264+ */
265265+interface ErrorDef {
266266+ /** Error name/code */
267267+ name: string;
268268+ /** Human-readable error description */
269269+ description?: string;
270270+}
271271+272272+/**
273273+ * Query endpoint options (HTTP GET).
274274+ * @see https://atproto.com/specs/lexicon#query
275275+ */
276276+interface QueryOptions {
277277+ /** Human-readable description */
278278+ description?: string;
279279+ /** Query string parameters */
280280+ parameters?: ParamsResult<ParamsProperties>;
281281+ /** Response body schema */
282282+ output?: BodySchema;
283283+ /** Possible error responses */
284284+ errors?: ErrorDef[];
285285+}
286286+287287+/**
288288+ * Procedure endpoint options (HTTP POST).
289289+ * @see https://atproto.com/specs/lexicon#procedure
290290+ */
291291+interface ProcedureOptions {
292292+ /** Human-readable description */
293293+ description?: string;
294294+ /** Query string parameters */
295295+ parameters?: ParamsResult<ParamsProperties>;
296296+ /** Request body schema */
297297+ input?: BodySchema;
298298+ /** Response body schema */
299299+ output?: BodySchema;
300300+ /** Possible error responses */
301301+ errors?: ErrorDef[];
302302+}
303303+304304+/**
305305+ * WebSocket message schema for subscriptions.
306306+ * @see https://atproto.com/specs/lexicon#subscription
307307+ */
308308+interface MessageSchema {
309309+ /** Human-readable description */
310310+ description?: string;
311311+ /** Union of possible message types */
312312+ schema: { type: "union"; refs: readonly string[] };
313313+}
314314+315315+/**
316316+ * Subscription endpoint options (WebSocket).
317317+ * @see https://atproto.com/specs/lexicon#subscription
318318+ */
319319+interface SubscriptionOptions {
320320+ /** Human-readable description */
321321+ description?: string;
322322+ /** Query string parameters */
323323+ parameters?: ParamsResult<ParamsProperties>;
324324+ /** Message schema for events */
325325+ message?: MessageSchema;
326326+ /** Possible error responses */
327327+ errors?: ErrorDef[];
328328+}
329329+330330+class Namespace<T extends LexiconNamespace> {
331331+ public json: T;
332332+ public infer: Infer<T> = null as unknown as Infer<T>;
333333+334334+ constructor(json: T) {
335335+ this.json = json;
336336+ }
337337+}
338338+339339+/**
340340+ * Main API for creating lexicon schemas.
341341+ * @see https://atproto.com/specs/lexicon
342342+ */
343343+export const lx = {
344344+ /**
345345+ * Creates a null type.
346346+ * @see https://atproto.com/specs/lexicon#null
347347+ */
348348+ null(
349349+ options?: LexiconItemCommonOptions,
350350+ ): { type: "null" } & LexiconItemCommonOptions {
351351+ return {
352352+ type: "null",
353353+ ...options,
354354+ };
355355+ },
356356+ /**
357357+ * Creates a boolean type with optional constraints.
358358+ * @see https://atproto.com/specs/lexicon#boolean
359359+ */
360360+ boolean<T extends BooleanOptions>(options?: T): T & { type: "boolean" } {
361361+ return {
362362+ type: "boolean",
363363+ ...options,
364364+ } as T & { type: "boolean" };
365365+ },
366366+ /**
367367+ * Creates an integer type with optional min/max and enum constraints.
368368+ * @see https://atproto.com/specs/lexicon#integer
369369+ */
370370+ integer<T extends IntegerOptions>(options?: T): T & { type: "integer" } {
371371+ return {
372372+ type: "integer",
373373+ ...options,
374374+ } as T & { type: "integer" };
375375+ },
376376+ /**
377377+ * Creates a string type with optional format, length, and value constraints.
378378+ * @see https://atproto.com/specs/lexicon#string
379379+ */
380380+ string<T extends StringOptions>(options?: T): T & { type: "string" } {
381381+ return {
382382+ type: "string",
383383+ ...options,
384384+ } as T & { type: "string" };
385385+ },
386386+ /**
387387+ * Creates an unknown type for flexible, unvalidated objects.
388388+ * @see https://atproto.com/specs/lexicon#unknown
389389+ */
390390+ unknown(
391391+ options?: LexiconItemCommonOptions,
392392+ ): { type: "unknown" } & LexiconItemCommonOptions {
393393+ return {
394394+ type: "unknown",
395395+ ...options,
396396+ };
397397+ },
398398+ /**
399399+ * Creates a bytes type for arbitrary byte arrays.
400400+ * @see https://atproto.com/specs/lexicon#bytes
401401+ */
402402+ bytes<T extends BytesOptions>(options?: T): T & { type: "bytes" } {
403403+ return {
404404+ type: "bytes",
405405+ ...options,
406406+ } as T & { type: "bytes" };
407407+ },
408408+ /**
409409+ * Creates a CID link reference to content-addressed data.
410410+ * @see https://atproto.com/specs/lexicon#cid-link
411411+ */
412412+ cidLink<Link extends string>(link: Link): { type: "cid-link"; $link: Link } {
413413+ return {
414414+ type: "cid-link",
415415+ $link: link,
416416+ };
417417+ },
418418+ /**
419419+ * Creates a blob type for binary data with MIME type constraints.
420420+ * @see https://atproto.com/specs/lexicon#blob
421421+ */
422422+ blob<T extends BlobOptions>(options?: T): T & { type: "blob" } {
423423+ return {
424424+ type: "blob",
425425+ ...options,
426426+ } as T & { type: "blob" };
427427+ },
428428+ /**
429429+ * Creates an array type with item schema and length constraints.
430430+ * @see https://atproto.com/specs/lexicon#array
431431+ */
432432+ array<Items extends { type: LexiconType }, Options extends ArrayOptions>(
433433+ items: Items,
434434+ options?: Options,
435435+ ): Options & { type: "array"; items: Items } {
436436+ return {
437437+ type: "array",
438438+ items,
439439+ ...options,
440440+ } as Options & { type: "array"; items: Items };
441441+ },
442442+ /**
443443+ * Creates a token type for symbolic values in unions.
444444+ * @see https://atproto.com/specs/lexicon#token
445445+ */
446446+ token<Description extends string>(
447447+ description: Description,
448448+ ): { type: "token"; description: Description } {
449449+ return { type: "token", description };
450450+ },
451451+ /**
452452+ * Creates a reference to another schema definition.
453453+ * @see https://atproto.com/specs/lexicon#ref
454454+ */
455455+ ref<Ref extends string>(
456456+ ref: Ref,
457457+ options?: LexiconItemCommonOptions,
458458+ ): LexiconItemCommonOptions & { type: "ref"; ref: Ref } {
459459+ return {
460460+ type: "ref",
461461+ ref,
462462+ ...options,
463463+ } as LexiconItemCommonOptions & { type: "ref"; ref: Ref };
464464+ },
465465+ /**
466466+ * Creates a union type for multiple possible type variants.
467467+ * @see https://atproto.com/specs/lexicon#union
468468+ */
469469+ union<const Refs extends readonly string[], Options extends UnionOptions>(
470470+ refs: Refs,
471471+ options?: Options,
472472+ ): Options & { type: "union"; refs: Refs } {
473473+ return {
474474+ type: "union",
475475+ refs,
476476+ ...options,
477477+ } as Options & { type: "union"; refs: Refs };
478478+ },
479479+ /**
480480+ * Creates a record type for repository records.
481481+ * @see https://atproto.com/specs/lexicon#record
482482+ */
483483+ record<T extends RecordOptions>(options: T): T & { type: "record" } {
484484+ return {
485485+ type: "record",
486486+ ...options,
487487+ };
488488+ },
489489+ /**
490490+ * Creates an object type with defined properties.
491491+ * @see https://atproto.com/specs/lexicon#object
492492+ */
493493+ object<T extends ObjectProperties>(options: T): ObjectResult<T> {
494494+ const required = Object.keys(options).filter(
495495+ (key) => "required" in options[key] && options[key].required,
496496+ );
497497+ const nullable = Object.keys(options).filter(
498498+ (key) => "nullable" in options[key] && options[key].nullable,
499499+ );
500500+ const result: Record<string, unknown> = {
501501+ type: "object",
502502+ properties: options,
503503+ };
504504+ if (required.length > 0) {
505505+ result.required = required;
506506+ }
507507+ if (nullable.length > 0) {
508508+ result.nullable = nullable;
509509+ }
510510+ return result as ObjectResult<T>;
511511+ },
512512+ /**
513513+ * Creates a params type for query string parameters.
514514+ * @see https://atproto.com/specs/lexicon#params
515515+ */
516516+ params<Properties extends ParamsProperties>(
517517+ properties: Properties,
518518+ ): ParamsResult<Properties> {
519519+ const required = Object.keys(properties).filter(
520520+ (key) => properties[key].required,
521521+ );
522522+ const result: Record<string, unknown> = {
523523+ type: "params",
524524+ properties,
525525+ };
526526+ if (required.length > 0) {
527527+ result.required = required;
528528+ }
529529+ return result as ParamsResult<Properties>;
530530+ },
531531+ /**
532532+ * Creates a query endpoint definition (HTTP GET).
533533+ * @see https://atproto.com/specs/lexicon#query
534534+ */
535535+ query<T extends QueryOptions>(options?: T): T & { type: "query" } {
536536+ return {
537537+ type: "query",
538538+ ...options,
539539+ } as T & { type: "query" };
540540+ },
541541+ /**
542542+ * Creates a procedure endpoint definition (HTTP POST).
543543+ * @see https://atproto.com/specs/lexicon#procedure
544544+ */
545545+ procedure<T extends ProcedureOptions>(
546546+ options?: T,
547547+ ): T & { type: "procedure" } {
548548+ return {
549549+ type: "procedure",
550550+ ...options,
551551+ } as T & { type: "procedure" };
552552+ },
553553+ /**
554554+ * Creates a subscription endpoint definition (WebSocket).
555555+ * @see https://atproto.com/specs/lexicon#subscription
556556+ */
557557+ subscription<T extends SubscriptionOptions>(
558558+ options?: T,
559559+ ): T & { type: "subscription" } {
560560+ return {
561561+ type: "subscription",
562562+ ...options,
563563+ } as T & { type: "subscription" };
564564+ },
565565+ /**
566566+ * Creates a lexicon namespace document.
567567+ * @see https://atproto.com/specs/lexicon#lexicon-document
568568+ */
569569+ namespace<ID extends string, D extends LexiconNamespace["defs"]>(
570570+ id: ID,
571571+ defs: D,
572572+ ): Namespace<{ lexicon: 1; id: ID; defs: D }> {
573573+ return new Namespace({
574574+ lexicon: 1,
575575+ id,
576576+ defs,
577577+ });
578578+ },
579579+};
+19
packages/prototypey/src/type-utils.ts
···11+/**
22+ * Converts a string union type to a tuple type
33+ * @example
44+ * type Colors = "red" | "green" | "blue";
55+ * type ColorTuple = UnionToTuple<Colors>; // ["red", "green", "blue"]
66+ */
77+export type UnionToTuple<T> = (
88+ (T extends unknown ? (x: () => T) => void : never) extends (
99+ x: infer I,
1010+ ) => void
1111+ ? I
1212+ : never
1313+) extends () => infer R
1414+ ? [...UnionToTuple<Exclude<T, R>>, R]
1515+ : [];
1616+1717+export type Prettify<T> = {
1818+ [K in keyof T]: T[K];
1919+} & {};