Skip to main content

ValidateRecursiveSchema TypeAlias

Compile time check for validity of a recursive schema. This type also serves as a central location for documenting the requirements and issues related to 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

In this context recursive schema are defined as all FieldSchema and TreeNodeSchema schema which are part of a cycle such that walking down through each childTypes the given starting schema can be reached again. Schema referencing the recursive schema and schema they reference that are not part of a cycle are not considered recursive.

TypeScript puts a lot of limitations on the typing of recursive schema. To help avoid running into these limitations and thus getting schema that do not type check (or only type checks sometimes!), SchemaFactory provides APIs (postfixed with Recursive) for writing recursive schema. These APIs when combined with the patterns documented below should ensure that the schema provide robust type checking. These special patterns (other than LazyItem forward references which are not recursion specific) are not required for correct runtime behavior: they exist entirely to mitigate TypeScript type checking limitations and bugs. Ideally TypeScript's type checker would be able to handle all of these cases and more, removing the need for recursive type specific guidance, rules and APIs. Currently however there are open issues preventing this: 1, 2, 3. Note that the proposed resolution to some of these issues is for the compiler to error rather than allow the case, so even if these are all resolved the recursive type workarounds may still be needed.

# Patterns

Below are patterns for how to use recursive schema.

## General Patterns

When defining a recursive TreeNodeSchema, use the *Recursive SchemaFactory methods. The returned class should be used as the base class for the recursive schema, which should then be passed to ValidateRecursiveSchema.

Using ValidateRecursiveSchema will provide compile error 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).

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 a reference to a schema is malformed (in this case boxed inside an object):

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.
}

If your TypeScript configuration objects to this patten due to the unused local, you can use allowUnused(t) to suppress the error:

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

## Object Schema

When defining fields, if the fields is part of the recursive cycle, use the *Recursive SchemaFactory methods for defining the FieldSchema.

## Array Schema

See FixRecursiveArraySchema for array specific details.

Example

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