Skip to main content

ValidateRecursiveSchema TypeAlias

Compile time check for validity of a recursive schema.

Signature

export type ValidateRecursiveSchema<T extends TreeNodeSchemaClass<string, NodeKind.Array | NodeKind.Map | NodeKind.Object, TreeNode & WithType<T["identifier"], T["kind"]>, {
[NodeKind.Object]: T["info"] extends RestrictiveStringRecord<ImplicitFieldSchema> ? InsertableObjectFromSchemaRecord<T["info"]> : unknown;
[NodeKind.Array]: T["info"] extends ImplicitAllowedTypes ? Iterable<InsertableTreeNodeFromImplicitAllowedTypes<T["info"]>> : unknown;
[NodeKind.Map]: T["info"] extends ImplicitAllowedTypes ? Iterable<[string, InsertableTreeNodeFromImplicitAllowedTypes<T["info"]>]> : unknown;
}[T["kind"]], false, {
[NodeKind.Object]: RestrictiveStringRecord<ImplicitFieldSchema>;
[NodeKind.Array]: ImplicitAllowedTypes;
[NodeKind.Map]: ImplicitAllowedTypes;
}[T["kind"]]>> = true;

Type Parameters

Parameter Constraint Description
T TreeNodeSchemaClass<string, NodeKind.Array | NodeKind.Map | NodeKind.Object, TreeNode & WithType<T["identifier"], T["kind"]>, { [NodeKind.Object]: T["info"] extends RestrictiveStringRecord<ImplicitFieldSchema> ? InsertableObjectFromSchemaRecord<T["info"]> : unknown; [NodeKind.Array]: T["info"] extends ImplicitAllowedTypes ? Iterable<InsertableTreeNodeFromImplicitAllowedTypes<T["info"]>> : unknown; [NodeKind.Map]: T["info"] extends ImplicitAllowedTypes ? Iterable<[string, InsertableTreeNodeFromImplicitAllowedTypes<T["info"]>]> : unknown; }[T["kind"]], false, { [NodeKind.Object]: RestrictiveStringRecord<ImplicitFieldSchema>; [NodeKind.Array]: ImplicitAllowedTypes; [NodeKind.Map]: ImplicitAllowedTypes; }[T["kind"]]>

Remarks

The type of a recursive schema can be passed to this, and a compile error will be produced for some of the cases of malformed schema. This can be used to help mitigate the issue that recursive schema definitions are Unenforced. If an issue is encountered where a mistake in a recursive schema is made which produces an invalid schema but is not rejected by this checker, it should be considered a bug and this should be updated to handle that case (or have a disclaimer added to these docs that it misses that case).

# Recursive Schema

The non-recursive versions of the schema building methods will run into several issues when used recursively. Consider the following example:

const Test = sf.array(Test); // Bad

This has several issues:

  1. It is a structurally named schema. Structurally named schema derive their name from the names of their child types, which is not possible when the type is recursive since its name would include itself. Instead a name must be explicitly provided.

  2. The schema accesses itself before it's defined. This would be a runtime error if the TypeScript compiler allowed it. This can be fixed by wrapping the type in a function, which also requires explicitly listing the allowed types in an array ([() => Test]).

  3. TypeScript fails to infer the recursive type and falls back to any with this warning or error (depending on the compiler configuration): 'Test' implicitly has type 'any' because it does not have a type annotation and is referenced directly or indirectly in its own initializer.ts(7022). This issue is what the specialized recursive schema building methods fix. This fix comes at a cost: to make the recursive cases work, the extends clauses had to be removed. This means that mistakes declaring recursive schema often don't give compile errors in the schema. Additionally support for implicit construction had to be disabled. This means that new nested Unhydrated nodes can not be created like new Test([[]]). Instead the nested nodes must be created explicitly using the construction likenew Test([new Test([])]).

  4. It is using "POJO" mode since it's not explicitly declaring a new class. This means that the generated d.ts files for the schema replace recursive references with any, breaking use of recursive schema across compilation boundaries. This is fixed by explicitly creating a class which extends the returned schema.

All together, the fixed version looks like:

class Test extends sf.arrayRecursive("Test", [() => Test]) {} // Good

Be very careful when declaring recursive schema. Due to the removed extends clauses, subtle mistakes will compile just fine but cause strange errors when the schema is used.

For example if the square brackets around the allowed types are forgotten:

class Test extends sf.arrayRecursive("Test", () => Test) {} // Bad

This schema will still compile, and some (but not all) usages of it may look like they work correctly while other usages will produce generally unintelligible compile errors. This issue can be partially mitigated using ValidateRecursiveSchema:

class Test extends sf.arrayRecursive("Test", () => Test) {} // Bad
{
type _check = ValidateRecursiveSchema<typeof Test>; // Reports compile error due to invalid schema above.
}

Example

class Test extends sf.arrayRecursive("Test", [() => Test]) {}
{
type _check = ValidateRecursiveSchema<typeof Test>;
}