Skip to content

Commit 5a08845

Browse files
Add some (mostly failing) generic schema type tests (#23936)
## Description This adds some tests which are generically parameterized over schema types. These are inspired by ongoing experiments with a Table schema generator, and issues encountered in its implementation. Generally it seems that we have some cases where we would really like TypeScript to simplify conditional type expressions based on extends clauses, but it does not do so causing code which would otherwise type check to fail. One such cause is covered in microsoft/TypeScript#52144 (comment) but there are likely others.
1 parent 703a821 commit 5a08845

File tree

1 file changed

+177
-0
lines changed

1 file changed

+177
-0
lines changed

packages/dds/tree/src/test/simple-tree/schemaTypes.spec.ts

+177
Original file line numberDiff line numberDiff line change
@@ -19,6 +19,7 @@ import {
1919
} from "../../simple-tree/index.js";
2020
import {
2121
type AllowedTypes,
22+
type FieldKind,
2223
type FieldSchema,
2324
type ImplicitAllowedTypes,
2425
type ImplicitFieldSchema,
@@ -45,6 +46,7 @@ import type {
4546
// eslint-disable-next-line import/no-internal-modules
4647
import { objectSchema } from "../../simple-tree/objectNode.js";
4748
import { validateUsageError } from "../utils.js";
49+
import { TreeAlpha } from "../../shared-tree/index.js";
4850

4951
const schema = new SchemaFactory("com.example");
5052

@@ -102,6 +104,15 @@ describe("schemaTypes", () => {
102104
type _check6 = requireTrue<areSafelyAssignable<I8, never>>;
103105
type _check7 = requireTrue<areSafelyAssignable<I9, never>>;
104106
type _check8 = requireTrue<areSafelyAssignable<I10, never>>;
107+
108+
// eslint-disable-next-line no-inner-declarations
109+
function _generic<T extends ImplicitAllowedTypes>() {
110+
type I14 = InsertableTreeFieldFromImplicitField<T>;
111+
type IOptional = InsertableTreeFieldFromImplicitField<
112+
FieldSchema<FieldKind.Optional, T>
113+
>;
114+
type _check9 = requireAssignableTo<undefined, IOptional>;
115+
}
105116
}
106117

107118
// InsertableTreeNodeFromImplicitAllowedTypes
@@ -217,6 +228,17 @@ describe("schemaTypes", () => {
217228
// boolean is sometimes a union of true and false, so it can break in its owns special ways
218229
type I13 = InsertableField<typeof booleanSchema>;
219230
type _check13 = requireTrue<areSafelyAssignable<I13, boolean>>;
231+
232+
// eslint-disable-next-line no-inner-declarations
233+
function _generic<T extends ImplicitAllowedTypes>() {
234+
type I14 = InsertableField<T>;
235+
type IOptional = InsertableField<FieldSchema<FieldKind.Optional, T>>;
236+
237+
// Most likely due to the TypeScript conditional type limitation described in https://github.com/microsoft/TypeScript/issues/52144#issuecomment-2686250788
238+
// This does not compile. Ideally this would compile:
239+
// @ts-expect-error Compiler limitation.
240+
type _check9 = requireAssignableTo<undefined, IOptional>;
241+
}
220242
}
221243

222244
// NodeFromSchema
@@ -524,4 +546,159 @@ describe("schemaTypes", () => {
524546
); // Different custom metadata
525547
check(sf.identifier, sf.optional(sf.string), false); // Identifier vs. optional string
526548
});
549+
550+
/**
551+
* Tests for patterns for making generically parameterized schema.
552+
*
553+
* Since the schema themselves can not be generic (at least not in a way thats captured in the stored schema),
554+
* this is done by making generic functions that return schema.
555+
*
556+
* Authoring such functions involves passing generic type parameters into the various schema type utilities,
557+
* and this causes some issues with many of them.
558+
*/
559+
describe("Generic Schema", () => {
560+
// Many of these cases should compile, but don't.
561+
// This is likely due to `[FieldSchema<Kind, T>] extends [ImplicitFieldSchema] ? TrueCase : FalseCase` not getting reduced to `TrueCase`.
562+
// This could be due to the compiler limitation noted in https://github.com/microsoft/TypeScript/issues/52144#issuecomment-2686250788
563+
564+
/**
565+
* Tests where the generic code constructs TreeNodes for the generic it's defining.
566+
* This scenario seems to be particularly problematic as the {@link Input} types seems to perform especially poorly due
567+
* to them using non distributive conditional types, which hits the issue noted above.
568+
*/
569+
it("Generic container construction", () => {
570+
const sf = new SchemaFactory("test");
571+
572+
/**
573+
* Define a generic container which holds the provided `T` directly as an implicit field schema.
574+
*/
575+
function makeInstanceImplicit<T extends ImplicitAllowedTypes>(
576+
schemaTypes: T,
577+
content: InsertableTreeFieldFromImplicitField<T>,
578+
) {
579+
class GenericContainer extends sf.object("GenericContainer", {
580+
content: schemaTypes,
581+
}) {}
582+
583+
// Both create and the constructor type check as desired.
584+
const _created = TreeAlpha.create(GenericContainer, { content });
585+
return new GenericContainer({ content });
586+
}
587+
588+
/**
589+
* Define a generic container which holds the provided `T` in a required field.
590+
*
591+
* This should function identically to the implicit one, but it doesn't.
592+
*/
593+
function makeInstanceRequired<T extends ImplicitAllowedTypes>(
594+
schemaTypes: T,
595+
content: InsertableTreeFieldFromImplicitField<T>,
596+
) {
597+
class GenericContainer extends sf.object("GenericContainer", {
598+
content: sf.required(schemaTypes),
599+
}) {}
600+
601+
// Users of the class (if it were returned from this test function with a concrete type instead of a generic one) would be fine,
602+
// but using it in this generic context has issues.
603+
// Specifically the construction APIs don't type check as desired.
604+
605+
// @ts-expect-error Compiler limitation, see comment above.
606+
const _created = TreeAlpha.create(GenericContainer, { content });
607+
// @ts-expect-error Compiler limitation, see comment above.
608+
return new GenericContainer({ content });
609+
}
610+
611+
/**
612+
* Define a generic container which holds the provided `T` in an optional field.
613+
*/
614+
function makeInstanceOptional<T extends ImplicitAllowedTypes>(
615+
schemaTypes: T,
616+
content: InsertableTreeFieldFromImplicitField<T> | undefined,
617+
) {
618+
class GenericContainer extends sf.object("GenericContainer", {
619+
content: sf.optional(schemaTypes),
620+
}) {}
621+
622+
// Like with the above case, TypeScript fails to simplify the input types, and these do not build.
623+
624+
// @ts-expect-error Compiler limitation, see comment above.
625+
const _createdEmpty = TreeAlpha.create(GenericContainer, { content: undefined });
626+
// @ts-expect-error Compiler limitation, see comment above.
627+
const _created = TreeAlpha.create(GenericContainer, { content });
628+
// @ts-expect-error Compiler limitation, see comment above.
629+
const _constructedEmpty = new GenericContainer({ content: undefined });
630+
// @ts-expect-error Compiler limitation, see comment above.
631+
return new GenericContainer({ content });
632+
}
633+
634+
/**
635+
* Define a generic container which holds the provided `T` in an optional field, using objectRecursive.
636+
* This case is included to highlight one scenario where the compiler limitation does not occur due to simpler typing.
637+
*/
638+
function makeInstanceOptionalRecursive<T extends ImplicitAllowedTypes>(
639+
schemaTypes: T,
640+
content: InsertableTreeFieldFromImplicitField<T> | undefined,
641+
) {
642+
class GenericContainer extends sf.objectRecursive("GenericContainer", {
643+
content: sf.optional(schemaTypes),
644+
}) {}
645+
646+
// @ts-expect-error Compiler limitation, see comment above.
647+
const _createdEmpty = TreeAlpha.create(GenericContainer, { content: undefined });
648+
// @ts-expect-error Compiler limitation, see comment above.
649+
const _created = TreeAlpha.create(GenericContainer, { content });
650+
const _constructedEmpty = new GenericContainer({ content: undefined }); // This one works.
651+
// @ts-expect-error Compiler limitation, see comment above.
652+
return new GenericContainer({ content });
653+
}
654+
});
655+
656+
it("Generic InsertableTreeFieldFromImplicitField", <T extends ImplicitAllowedTypes>() => {
657+
type Required = FieldSchema<FieldKind.Required, T>;
658+
659+
type ArgFieldImplicit2 = InsertableTreeFieldFromImplicitField<T>;
660+
type ArgFieldRequired2 = InsertableTreeFieldFromImplicitField<Required>;
661+
662+
// We would expect a required field and an implicitly required field to have the same types.
663+
// This is normally true, but is failing when the schema is generic due to the compiler limitation noted above.
664+
665+
// @ts-expect-error Compiler limitation, see comment above.
666+
type _check5 = requireAssignableTo<ArgFieldRequired2, ArgFieldImplicit2>;
667+
// @ts-expect-error Compiler limitation, see comment above.
668+
type _check6 = requireAssignableTo<ArgFieldImplicit2, ArgFieldRequired2>;
669+
});
670+
671+
it("Generic TreeFieldFromImplicitField", <T extends ImplicitAllowedTypes>() => {
672+
type Required = FieldSchema<FieldKind.Required, T>;
673+
674+
type ArgFieldImplicit2 = TreeFieldFromImplicitField<T>;
675+
type ArgFieldRequired2 = TreeFieldFromImplicitField<Required>;
676+
677+
// We would expect a required field and an implicitly required field to have the same types.
678+
// This is normally true, but is failing when the schema is generic due to the compiler limitation noted above.
679+
// This case is for the node types not the insertable ones, so it was more likely to work, but still fails.
680+
681+
// @ts-expect-error Compiler limitation, see comment above.
682+
type _check5 = requireAssignableTo<ArgFieldRequired2, ArgFieldImplicit2>;
683+
// @ts-expect-error Compiler limitation, see comment above.
684+
type _check6 = requireAssignableTo<ArgFieldImplicit2, ArgFieldRequired2>;
685+
});
686+
687+
it("Generic optional field", <T extends ImplicitAllowedTypes>() => {
688+
type Optional = FieldSchema<FieldKind.Optional, T>;
689+
690+
type ArgFieldImplicit = InsertableTreeFieldFromImplicitField<T>;
691+
type ArgFieldOptional = InsertableTreeFieldFromImplicitField<Optional>;
692+
693+
// An optional field should be the same as a required field unioned with undefined. Typescript fails to see this when its generic:
694+
695+
// @ts-expect-error Compiler limitation, see comment above.
696+
type _check5 = requireAssignableTo<ArgFieldOptional, ArgFieldImplicit | undefined>;
697+
// @ts-expect-error Compiler limitation, see comment above.
698+
type _check6 = requireAssignableTo<ArgFieldImplicit | undefined, ArgFieldOptional>;
699+
700+
// At least this case allows undefined, like recursive object fields, but unlike non recursive object fields.
701+
type _check7 = requireAssignableTo<undefined, ArgFieldOptional>;
702+
});
703+
});
527704
});

0 commit comments

Comments
 (0)