Skip to content

Commit 463a6a9

Browse files
committed
refactor: switch from Brand.Type to concrete $type field
1 parent c207c28 commit 463a6a9

5 files changed

Lines changed: 739 additions & 932 deletions

File tree

packages/lex-cli/src/generator/index.ts

Lines changed: 11 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -81,8 +81,7 @@ export async function generateDefinitions(opts: GenerateDefinitionsOptions) {
8181
const descs = getDescriptions(def);
8282

8383
chunk += writeJsdoc(descs);
84-
chunk += `interface ${typeName} {`;
85-
chunk += `[Brand.Type]: '${nsid}';`;
84+
chunk += `interface ${typeName} extends TypedBase {`;
8685

8786
for (const prop of propKeys) {
8887
const isOptional = !required || !required.includes(prop);
@@ -123,7 +122,7 @@ export async function generateDefinitions(opts: GenerateDefinitionsOptions) {
123122
const descs = getDescriptions(def);
124123

125124
chunk += writeJsdoc(descs);
126-
chunk += 'interface Record {';
125+
chunk += `interface Record extends RecordBase {`;
127126
chunk += `$type: '${nsid}';`;
128127

129128
for (const prop of propKeys) {
@@ -158,11 +157,11 @@ export async function generateDefinitions(opts: GenerateDefinitionsOptions) {
158157
const { value, descriptions } = resolveType(nsid, parameters);
159158

160159
chunk += writeJsdoc(descriptions);
161-
chunk += `interface Params ${value}`;
160+
chunk += `interface Params extends TypedBase ${value}`;
162161
}
163162
}
164163
else {
165-
chunk += 'interface Params {}';
164+
chunk += 'interface Params extends TypedBase {}';
166165
}
167166

168167
if (input) {
@@ -172,7 +171,7 @@ export async function generateDefinitions(opts: GenerateDefinitionsOptions) {
172171
chunk += writeJsdoc(descriptions);
173172

174173
if (input.schema?.type === 'object') {
175-
chunk += `interface Input ${value}`;
174+
chunk += `interface Input extends TypedBase ${value}`;
176175
}
177176
else {
178177
chunk += `type Input = ${value};`;
@@ -193,7 +192,7 @@ export async function generateDefinitions(opts: GenerateDefinitionsOptions) {
193192
chunk += writeJsdoc(descriptions);
194193

195194
if (output.schema?.type === 'object') {
196-
chunk += `interface Output ${value}`;
195+
chunk += `interface Output extends TypedBase ${value}`;
197196
}
198197
else {
199198
chunk += `type Output = ${value};`;
@@ -208,7 +207,7 @@ export async function generateDefinitions(opts: GenerateDefinitionsOptions) {
208207
}
209208

210209
if (errors) {
211-
chunk += 'interface Errors {';
210+
chunk += 'interface Errors extends TypedBase {';
212211

213212
for (const error of errors) {
214213
chunk += `${error.name}: {};`;
@@ -256,10 +255,10 @@ export async function generateDefinitions(opts: GenerateDefinitionsOptions) {
256255
if (def.parameters) {
257256
const { value, descriptions } = resolveType(nsid, def.parameters);
258257
chunk += writeJsdoc(descriptions);
259-
chunk += `interface Params ${value}`;
258+
chunk += `interface Params extends TypedBase ${value}`;
260259
}
261260
else {
262-
chunk += 'interface Params {}';
261+
chunk += 'interface Params extends TypedBase {}';
263262
}
264263

265264
if (def.message?.schema) {
@@ -270,7 +269,7 @@ export async function generateDefinitions(opts: GenerateDefinitionsOptions) {
270269
}
271270

272271
if (def.errors) {
273-
chunk += 'interface Errors {';
272+
chunk += 'interface Errors extends TypedBase {';
274273
for (const error of def.errors) {
275274
chunk += `${error.name}: {};`;
276275
}
@@ -302,7 +301,7 @@ export async function generateDefinitions(opts: GenerateDefinitionsOptions) {
302301
code += '}\n\n';
303302
}
304303

305-
code += `export declare interface Records {${records}}\n\n`;
304+
code += `export declare interface Records extends RecordBase {${records}}\n\n`;
306305
code += `export declare interface Queries {${queries}}\n\n`;
307306
code += `export declare interface Procedures {${procedures}}\n\n`;
308307
code += `export declare interface Subscriptions {${subscriptions}}\n\n`;

packages/lex-cli/src/generator/resolvers/complex.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -17,7 +17,7 @@ export function resolveUnionType(def: RefUnionSchema): string {
1717
const [ns, ref] = raw.split('#');
1818
return (ns ? `${toNamespace(ns)}.` : '') + (ref ? toUpper(ref) : 'Main');
1919
});
20-
return `Brand.Union<${refs.join('|')}>`;
20+
return `TypeUnion<${refs.join('|')}>`;
2121
}
2222

2323
export function resolveObjectType(

packages/lex-cli/src/utils/prelude.ts

Lines changed: 18 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -1,18 +1,24 @@
1-
export const mainPrelude = `type ObjectOmit<T, K extends keyof any> = Omit<T, K>;
1+
export const mainPrelude = `/** Base type with optional type field */
2+
export interface TypedBase {
3+
$type?: string;
4+
}
25
3-
/** Handles type branding in objects */
4-
export declare namespace Brand {
5-
/** Symbol used to brand objects, this does not actually exist in runtime */
6-
const Type: unique symbol;
6+
/** Base type for all record types */
7+
export interface RecordBase {
8+
$type: string;
9+
}
710
8-
/** Get the intended \`$type\` field */
9-
type GetType<T extends { [Type]: string }> = T[typeof Type];
11+
/** Makes $type required and specific */
12+
export type Typed<T extends TypedBase, Type extends string> = Omit<T, '$type'> & {
13+
$type: Type;
14+
};
1015
11-
/** Creates a union of objects where it's discriminated by \`$type\` field. */
12-
type Union<T extends { [Type]: string }> = T extends any ? T & { $type: GetType<T> } : never;
16+
/** Creates a union of objects discriminated by $type */
17+
export type TypeUnion<T extends TypedBase> = T extends any ? Typed<T, string> : never;
1318
14-
/** Omits the type branding from object */
15-
type Omit<T extends { [Type]: string }> = ObjectOmit<T, typeof Type>;
19+
/** Type guard for records */
20+
export function isRecord(value: unknown): value is RecordBase {
21+
return typeof value === 'object' && value !== null && '$type' in value && typeof value.$type === 'string';
1622
}
1723
1824
/** Base AT Protocol schema types */
@@ -40,7 +46,7 @@ export declare namespace At {
4046
}
4147
4248
/** Blob interface */
43-
interface Blob<T extends string = string> {
49+
interface Blob<T extends string = string> extends RecordBase {
4450
$type: 'blob';
4551
mimeType: T;
4652
ref: {

packages/lexicons/src/index.ts

Lines changed: 7 additions & 24 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
import type { Procedures, Queries, Records } from './lib/lexicons.js';
1+
import { isRecord, type Procedures, type Queries, type Records } from './lib/lexicons.js';
22

33
export * from './lib/lexicons.js';
44

@@ -16,8 +16,8 @@ export type KnownNSID = BskyNSID | AtProtoNSID;
1616

1717
// --- Record Types ---
1818
export type RecordDefs = LexiconUnion<Records>;
19-
export type BskyRecord = RecordDefs & { $type: BskyNSID };
20-
export type AtProtoRecord = RecordDefs & { $type: AtProtoNSID };
19+
export type BskyRecord = Extract<RecordDefs, { $type: BskyNSID }>;
20+
export type AtProtoRecord = Extract<RecordDefs, { $type: AtProtoNSID }>;
2121

2222
// --- Query Types ---
2323
export type QueryDefs = LexiconUnion<Queries>;
@@ -73,29 +73,12 @@ export type ProcedureErrors<T extends keyof Procedures> =
7373
: never;
7474

7575
// --- Common Bluesky Types ---
76-
export type BskyPost = BskyRecord & {
77-
$type: `${typeof APP_BSKY_PREFIX}feed.post`;
78-
};
79-
export type BskyProfile = BskyRecord & {
80-
$type: `${typeof APP_BSKY_PREFIX}actor.profile`;
81-
};
82-
export type BskyLike = BskyRecord & {
83-
$type: `${typeof APP_BSKY_PREFIX}feed.like`;
84-
};
85-
export type BskyFollow = BskyRecord & {
86-
$type: `${typeof APP_BSKY_PREFIX}graph.follow`;
87-
};
76+
export type BskyPost = Extract<BskyRecord, { $type: `${typeof APP_BSKY_PREFIX}feed.post` }>;
77+
export type BskyProfile = Extract<BskyRecord, { $type: `${typeof APP_BSKY_PREFIX}actor.profile` }>;
78+
export type BskyLike = Extract<BskyRecord, { $type: `${typeof APP_BSKY_PREFIX}feed.like` }>;
79+
export type BskyFollow = Extract<BskyRecord, { $type: `${typeof APP_BSKY_PREFIX}graph.follow` }>;
8880

8981
// --- Type Guards ---
90-
export function isRecord(value: unknown): value is RecordDefs {
91-
return (
92-
typeof value === 'object'
93-
&& value !== null
94-
&& '$type' in value
95-
&& typeof value.$type === 'string'
96-
);
97-
}
98-
9982
export function isBskyRecord(value: unknown): value is BskyRecord {
10083
return isRecord(value) && value.$type.startsWith(APP_BSKY_PREFIX);
10184
}

0 commit comments

Comments
 (0)