import * as l from "../mod.ts"; const Facet = l.union("blue.cerulea.app.facet", { "#mention": l.object({ start: l.integer, end: l.integer, did: l.did }), "#link": l.object({ start: l.integer, end: l.integer, uri: l.uri }), }); const Post = l.record("blue.cerulea.app.post", { key: "tid" }, { text: l.string, facets: l.array(Facet), createdAt: l.optional(l.datetime), nullable: l.nullable(l.string), both: l.optional(l.nullable(l.string)), }); type Equal = (() => T extends A ? 1 : 2) extends (() => T extends B ? 1 : 2) ? true : false; type Expect = T; export type DiscriminatedInference = [ Expect< Equal< l.Infer, | { readonly $type: "blue.cerulea.app.facet#mention"; readonly start: number; readonly end: number; readonly did: `did:${string}`; } | { readonly $type: "blue.cerulea.app.facet#link"; readonly start: number; readonly end: number; readonly uri: string; } > >, Expect< Equal, { readonly $type: "blue.cerulea.app.post"; readonly text: string; readonly facets: readonly l.Infer[]; readonly createdAt?: string; readonly nullable: string | null; readonly both?: string | null; }> >, Expect< Equal< l.Input, | { readonly $type: "blue.cerulea.app.facet#mention" | "#mention"; readonly start: number; readonly end: number; readonly did: `did:${string}`; } | { readonly $type: "blue.cerulea.app.facet#link" | "#link"; readonly start: number; readonly end: number; readonly uri: string; } > >, ]; // This function is type-checked but never called. export function inference(post: l.Infer, facet: l.Infer): void { const p: l.Infer = { $type: "blue.cerulea.app.post", text: "", facets: [], nullable: null, }; const built: l.Infer = Post.build({ text: "", facets: [], nullable: null, }); void built; // @ts-expect-error Required record fields cannot be omitted. Post.build({ text: "", facets: [] }); // @ts-expect-error Record fields must not supply their own discriminator. Post.build({ text: "", facets: [], nullable: null, $type: Post.id }); // @ts-expect-error Only records have builders. l.object({ text: l.string }).build({ text: "" }); const optional: string | undefined = p.createdAt; const nullable: string | null = p.nullable; const both: string | null | undefined = p.both; void [optional, nullable, both]; const checked = Post.validate(post); if (checked.success) { const value: l.Infer = checked.value; void value; } // @ts-expect-error Records require their inferred discriminator. const missing: l.Infer = { text: "", facets: [], nullable: null }; const undefinedField: l.Infer = { ...p, createdAt: undefined }; // @ts-expect-error Non-nullable optional fields do not accept null. const nullField: l.Infer = { ...p, createdAt: null }; // @ts-expect-error Nullable fields are still required. const missingNullable: l.Infer = { $type: "blue.cerulea.app.post", text: "", facets: [], }; void [missing, undefinedField, nullField, missingNullable]; // @ts-expect-error Inferred record properties are readonly. post.text = "changed"; // @ts-expect-error Inferred arrays are readonly. post.facets.push(facet); // @ts-expect-error Union discriminators cannot be changed. facet.$type = "blue.cerulea.app.facet#mention"; if (facet.$type === "blue.cerulea.app.facet#mention") { const did: `did:${string}` = facet.did; void did; // @ts-expect-error Mention narrowing excludes link fields. void facet.uri; } else { const uri: string = facet.uri; void uri; // @ts-expect-error Link narrowing excludes mention fields. void facet.did; } const named = l.named("blue.cerulea.app.defs#facetHolder", l.object({ facet: Facet })); const value: l.Infer = { facet }; // @ts-expect-error Named object refs do not add a discriminator. void value.$type; const open = l.union("blue.cerulea.app.open", { "#known": l.object({ text: l.string }) }, { closed: false, }); const future: l.Infer = { $type: "other.example.future", anything: 42 }; void future; const words = l.enumValues("yes", "no"); const word: l.Infer = "yes"; // @ts-expect-error Enums retain literal inference. const invalidWord: l.Infer = "maybe"; void [word, invalidWord]; const result = l.compile(Post)(post); if (result.success) { const sameType: l.Infer = result.value; void sameType; } // @ts-expect-error Optionality is only allowed on object properties. l.array(l.optional(l.string)); // @ts-expect-error Nullability is only allowed on object properties. l.array(l.nullable(l.string)); // @ts-expect-error Variants must be objects. l.union("blue.cerulea.app.facet", { "#text": l.string }); } // This function is type-checked but never called. export function building(facet: l.Infer): void { const built: l.Infer = Post.build({ text: "", facets: [ { $type: "#mention", start: 0, end: 1, did: "did:plc:alice" }, { $type: "blue.cerulea.app.facet#link", start: 0, end: 1, uri: "" }, facet, ], nullable: null, }); const single: l.Infer = Facet.build({ $type: "#link", start: 0, end: 1, uri: "" }); void [built, single]; // @ts-expect-error Short variants must name a declared variant. Facet.build({ $type: "#other", start: 0, end: 1, uri: "" }); // @ts-expect-error Short variants narrow to their variant's fields. Facet.build({ $type: "#mention", start: 0, end: 1, did: "did:plc:alice", uri: "" }); // @ts-expect-error Stored values need full discriminators. const short: l.Infer = { $type: "#link", start: 0, end: 1, uri: "" }; void short; const anonymous = l.union({ "blue.cerulea.app.facet#link": l.object({ uri: l.uri }) }); anonymous.build({ $type: "blue.cerulea.app.facet#link", uri: "" }); // @ts-expect-error Only namespaced unions accept short variants. anonymous.build({ $type: "#link", uri: "" }); const Embed = l.union("blue.cerulea.app.embed", { "#facets": l.object({ facets: l.array(Facet) }), "#post": l.object({ post: Post }), }); const Holder = l.record("blue.cerulea.app.holder", { key: "tid" }, { embed: l.optional(Embed), pinned: l.named("blue.cerulea.app.defs#pin", l.object({ facet: l.nullable(Facet) })), }); Holder.build({ embed: { $type: "#facets", facets: [{ $type: "#link", start: 0, end: 1, uri: "" }] }, pinned: { facet: { $type: "#mention", start: 0, end: 1, did: "did:plc:alice" } }, }); Holder.build({ embed: { $type: "#post", post: { text: "", facets: [], nullable: null } }, pinned: { facet: null }, }); Holder.build({ embed: { $type: "#post", post: { $type: Post.id, text: "", facets: [], nullable: null } }, pinned: { facet: null }, }); Holder.build({ embed: { $type: "#post", // @ts-expect-error Nested records only accept their own discriminator. post: { $type: "blue.cerulea.app.other", text: "", facets: [], nullable: null }, }, pinned: { facet: null }, }); const open = l.union("blue.cerulea.app.open", { "#known": l.object({ text: l.string }) }, { closed: false, }); open.build({ $type: "#known", text: "" }); open.build({ $type: "other.example.future", anything: 42 }); } const EagerNamed = l.named( "blue.cerulea.app.defs#eager", l.object({ facet: Facet }), ); const LazyNamed = l.named( "blue.cerulea.app.defs#lazy", () => l.object({ facet: Facet }), ); const NamedUnion = l.union([EagerNamed, LazyNamed]); export type NamedInference = [ Expect, l.Infer>>, Expect, l.Input>>, Expect>, Expect["facet"], l.Infer>>, Expect["facet"], l.Input>>, Expect< Equal< l.Infer, | { readonly $type: "blue.cerulea.app.defs#eager"; readonly facet: l.Infer; } | { readonly $type: "blue.cerulea.app.defs#lazy"; readonly facet: l.Infer; } > >, Expect< Equal< l.Input, | { readonly $type: "blue.cerulea.app.defs#eager"; readonly facet: l.Input; } | { readonly $type: "blue.cerulea.app.defs#lazy"; readonly facet: l.Input; } > >, ]; export function namedInference(): void { const Text = l.named( "blue.cerulea.app.defs#text", () => l.object({ text: l.string }), ); const Count = l.named( "blue.cerulea.app.defs#count", l.object({ count: l.integer }), ); const Result = l.union([Text, Count]); Result.build({ $type: Text.id, text: "hello" }); // @ts-expect-error Named union discriminators select their own fields. Result.build({ $type: Text.id, count: 1 }); // @ts-expect-error Array unions have no namespace for short discriminators. Result.build({ $type: "#text", text: "hello" }); // @ts-expect-error Array unions require named definitions. l.union([l.object({})]); const value = Result.build({ $type: Count.id, count: 1 }); if (value.$type === Count.id) { const count: number = value.count; void count; // @ts-expect-error Discriminator narrowing excludes the other variant's fields. void value.text; } type Tree = { readonly text: string; readonly children?: readonly Tree[] }; const Tree: l.NamedSchema<"blue.cerulea.app.defs#tree", Tree> = l.named( "blue.cerulea.app.defs#tree", () => l.object({ text: l.string, children: l.optional(l.array(Tree)) }), ); const Holder = l.record("blue.cerulea.app.tree", { key: "tid" }, { tree: Tree, }); Holder.build({ tree: { text: "root", children: [{ text: "leaf" }] } }); // @ts-expect-error Explicit recursive annotations still check nested fields. Holder.build({ tree: { text: "root", children: [{ text: 1 }] } }); // @ts-expect-error A recursive annotation must agree with the factory's schema. const Wrong: l.NamedSchema<"blue.cerulea.app.defs#wrong", Tree> = l.named( "blue.cerulea.app.defs#wrong", () => l.object({ text: l.integer }), ); void Wrong; }