Skip to content

Commit c9d32d6

Browse files
tree2: Cleanup creation of field schema (#17699)
## Description Adopts a new pattern for documenting workarounds for microsoft/TypeScript#55758, making the documentation more centralized, and more clear about what the desired extends clauses are. Also avoids using the constructor for FieldSchema and instead use static builders whose type parameters can be constrained. This, when combined with `const` generic type parameters removes the reason for existence of the generic FieldSchema builders on SchemaBuilder, so those have been removed (the ones which depend on specific field kinds are kept). Also adds some runtime validation in FieldSchema for cases the type system can't fully handle. ## Breaking Changes Users of SchemaBuilder.field (or new FieldSchema) and SchmeaBuilder.fieldRecursive should use FieldSchema.create and FieldSchema.createUnsafe.
1 parent ce11ba9 commit c9d32d6

35 files changed

+238
-202
lines changed

api-report/tree2.api.md

Lines changed: 23 additions & 27 deletions
Original file line numberDiff line numberDiff line change
@@ -681,10 +681,11 @@ interface Fields {
681681
}
682682

683683
// @alpha @sealed
684-
export class FieldSchema<out Kind extends FieldKind = FieldKind, const out Types = AllowedTypes> {
685-
constructor(kind: Kind, allowedTypes: Types);
684+
export class FieldSchema<out Kind extends FieldKind = FieldKind, const out Types extends Unenforced<AllowedTypes> = AllowedTypes> {
686685
// (undocumented)
687686
readonly allowedTypes: Types;
687+
static create<Kind extends FieldKind, const Types extends AllowedTypes>(kind: Kind, allowedTypes: Types): FieldSchema<Kind, Types>;
688+
static createUnsafe<Kind extends FieldKind, const Types>(kind: Kind, allowedTypes: Types): FieldSchema<Kind, Types>;
688689
static readonly empty: FieldSchema<Forbidden, readonly []>;
689690
equals(other: FieldSchema): boolean;
690691
// (undocumented)
@@ -933,8 +934,6 @@ declare namespace InternalTypedSchemaTypes {
933934
MapSchemaSpecification,
934935
LeafSchemaSpecification,
935936
MapFieldSchema,
936-
RecursiveTreeSchemaSpecification,
937-
RecursiveTreeSchema,
938937
FlexList,
939938
FlexListToNonLazyArray,
940939
ConstantFlexListToNonLazyArray,
@@ -1457,7 +1456,7 @@ interface NodeKeyField extends TreeField {
14571456

14581457
// @alpha
14591458
export const nodeKeyField: {
1460-
__n_id__: FieldSchema<NodeKeyFieldKind, [TreeSchema<"com.fluidframework.nodeKey.NodeKey", {
1459+
__n_id__: FieldSchema<NodeKeyFieldKind, readonly [TreeSchema<"com.fluidframework.nodeKey.NodeKey", {
14611460
leafValue: ValueSchema.String;
14621461
}>]>;
14631462
};
@@ -1667,8 +1666,8 @@ export function recordDependency(dependent: ObservingDependent | undefined, depe
16671666
// @alpha (undocumented)
16681667
const recursiveStruct: TreeSchema<"Test Recursive Domain.struct", {
16691668
structFields: {
1670-
recursive: FieldSchema<Optional, [() => TreeSchema<"Test Recursive Domain.struct", any>]>;
1671-
number: FieldSchema<Required_2, [TreeSchema<"com.fluidframework.leaf.number", {
1669+
readonly recursive: FieldSchema<Optional, readonly [() => TreeSchema<"Test Recursive Domain.struct", any>]>;
1670+
readonly number: FieldSchema<Required_2, readonly [TreeSchema<"com.fluidframework.leaf.number", {
16721671
leafValue: import("..").ValueSchema.Number;
16731672
}>]>;
16741673
};
@@ -1677,19 +1676,13 @@ leafValue: import("..").ValueSchema.Number;
16771676
// @alpha (undocumented)
16781677
const recursiveStruct2: TreeSchema<"Test Recursive Domain.struct2", {
16791678
structFields: {
1680-
readonly recursive: FieldSchema<Optional, [() => TreeSchema<"Test Recursive Domain.struct2", any>]>;
1681-
readonly number: FieldSchema<Required_2, [TreeSchema<"com.fluidframework.leaf.number", {
1679+
readonly recursive: FieldSchema<Optional, readonly [() => TreeSchema<"Test Recursive Domain.struct2", any>]>;
1680+
readonly number: FieldSchema<Required_2, readonly [TreeSchema<"com.fluidframework.leaf.number", {
16821681
leafValue: import("..").ValueSchema.Number;
16831682
}>]>;
16841683
};
16851684
}>;
16861685

1687-
// @alpha
1688-
type RecursiveTreeSchema = unknown;
1689-
1690-
// @alpha
1691-
type RecursiveTreeSchemaSpecification = unknown;
1692-
16931686
// @alpha
16941687
type _RecursiveTrick = never;
16951688

@@ -1773,37 +1766,37 @@ export { SchemaAware }
17731766

17741767
// @alpha @sealed
17751768
export class SchemaBuilder<TScope extends string = string, TName extends number | string = string> extends SchemaBuilderBase<TScope, TName> {
1776-
fieldNode<Name extends TName, T extends ImplicitFieldSchema>(name: Name, fieldSchema: T): TreeSchema<`${TScope}.${Name}`, {
1769+
fieldNode<Name extends TName, const T extends ImplicitFieldSchema>(name: Name, fieldSchema: T): TreeSchema<`${TScope}.${Name}`, {
17771770
structFields: {
17781771
[""]: NormalizeField_2<T, DefaultFieldKind>;
17791772
};
17801773
}>;
1781-
fieldNodeRecursive<Name extends TName, T>(name: Name, t: T): TreeSchema<`${TScope}.${Name}`, {
1774+
fieldNodeRecursive<Name extends TName, const T extends Unenforced<ImplicitFieldSchema>>(name: Name, t: T): TreeSchema<`${TScope}.${Name}`, {
17821775
structFields: {
17831776
[""]: T;
17841777
};
17851778
}>;
1786-
static fieldOptional<T extends AllowedTypes>(...allowedTypes: T): FieldSchema<typeof FieldKinds.optional, T>;
1787-
static fieldRequired<T extends AllowedTypes>(...allowedTypes: T): FieldSchema<typeof FieldKinds.required, T>;
1788-
static fieldSequence<T extends AllowedTypes>(...t: T): FieldSchema<typeof FieldKinds.sequence, T>;
1789-
leaf<Name extends TName, T extends ValueSchema>(name: Name, t: T): TreeSchema<`${TScope}.${Name}`, {
1779+
static fieldOptional<const T extends AllowedTypes>(...allowedTypes: T): FieldSchema<typeof FieldKinds.optional, T>;
1780+
static fieldRequired<const T extends AllowedTypes>(...allowedTypes: T): FieldSchema<typeof FieldKinds.required, T>;
1781+
static fieldSequence<const T extends AllowedTypes>(...t: T): FieldSchema<typeof FieldKinds.sequence, T>;
1782+
leaf<Name extends TName, const T extends ValueSchema>(name: Name, t: T): TreeSchema<`${TScope}.${Name}`, {
17901783
leafValue: T;
17911784
}>;
1792-
map<Name extends TName, T extends ImplicitFieldSchema>(name: Name, fieldSchema: T): TreeSchema<`${TScope}.${Name}`, {
1785+
map<Name extends TName, const T extends ImplicitFieldSchema>(name: Name, fieldSchema: T): TreeSchema<`${TScope}.${Name}`, {
17931786
mapFields: NormalizeField_2<T, DefaultFieldKind>;
17941787
}>;
1795-
mapRecursive<Name extends TName, T>(name: Name, t: T): TreeSchema<`${TScope}.${Name}`, {
1788+
mapRecursive<Name extends TName, const T extends Unenforced<ImplicitFieldSchema>>(name: Name, t: T): TreeSchema<`${TScope}.${Name}`, {
17961789
mapFields: T;
17971790
}>;
17981791
struct<const Name extends TName, const T extends RestrictiveReadonlyRecord<string, ImplicitFieldSchema>>(name: Name, t: T): TreeSchema<`${TScope}.${Name}`, {
17991792
structFields: {
18001793
[key in keyof T]: NormalizeField_2<T[key], DefaultFieldKind>;
18011794
};
18021795
}>;
1803-
structRecursive<Name extends TName, T>(name: Name, t: T): TreeSchema<`${TScope}.${Name}`, {
1796+
structRecursive<Name extends TName, const T extends Unenforced<RestrictiveReadonlyRecord<string, ImplicitFieldSchema>>>(name: Name, t: T): TreeSchema<`${TScope}.${Name}`, {
18041797
structFields: T;
18051798
}>;
1806-
toDocumentSchema<TSchema extends ImplicitFieldSchema>(root: TSchema): TypedSchemaCollection<NormalizeField_2<TSchema, DefaultFieldKind>>;
1799+
toDocumentSchema<const TSchema extends ImplicitFieldSchema>(root: TSchema): TypedSchemaCollection<NormalizeField_2<TSchema, DefaultFieldKind>>;
18071800
}
18081801

18091802
// @alpha
@@ -1817,7 +1810,7 @@ export class SchemaBuilderBase<TScope extends string, TName extends number | str
18171810
// (undocumented)
18181811
protected addNodeSchema<T extends TreeSchema<string, any>>(schema: T): void;
18191812
static field<Kind extends FieldKind, T extends AllowedTypes>(kind: Kind, ...allowedTypes: T): FieldSchema<Kind, T>;
1820-
static fieldRecursive<Kind extends FieldKind, T extends FlexList<RecursiveTreeSchema>>(kind: Kind, ...allowedTypes: T): FieldSchema<Kind, T>;
1813+
static fieldRecursive<Kind extends FieldKind, T extends FlexList<Unenforced<TreeSchema>>>(kind: Kind, ...allowedTypes: T): FieldSchema<Kind, T>;
18211814
finalize(): SchemaLibrary;
18221815
readonly name: string;
18231816
// (undocumented)
@@ -2103,7 +2096,7 @@ export interface TreeNode extends Tree<TreeSchema> {
21032096
}
21042097

21052098
// @alpha
2106-
export class TreeSchema<Name extends string = string, T extends RecursiveTreeSchemaSpecification = TreeSchemaSpecification> {
2099+
export class TreeSchema<Name extends string = string, T extends Unenforced<TreeSchemaSpecification> = TreeSchemaSpecification> {
21072100
constructor(builder: Named<string>, name: Name, info: T);
21082101
// (undocumented)
21092102
readonly builder: Named<string>;
@@ -2289,6 +2282,9 @@ TName extends infer S & TreeSchemaIdentifier ? S : string
22892282
// @alpha
22902283
type UnbrandList<T extends unknown[], B> = T extends [infer Head, ...infer Tail] ? [Unbrand<Head, B>, ...UnbrandList<Tail, B>] : [];
22912284

2285+
// @alpha
2286+
export type Unenforced<_DesiredExtendsConstraint> = unknown;
2287+
22922288
// @alpha
22932289
type UntypedApi<Mode extends ApiMode> = {
22942290
[ApiMode.Editable]: UntypedTree;

experimental/dds/tree2/src/domains/json/jsonDomainSchema.ts

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -50,15 +50,15 @@ export const jsonRoot = [() => jsonObject, () => jsonArray, ...jsonPrimitives] a
5050
*/
5151
export const jsonObject = builder.mapRecursive(
5252
"Object",
53-
new FieldSchema(FieldKinds.optional, jsonRoot),
53+
FieldSchema.createUnsafe(FieldKinds.optional, jsonRoot),
5454
);
5555

5656
/**
5757
* @alpha
5858
*/
5959
export const jsonArray = builder.fieldNodeRecursive(
6060
"Array",
61-
new FieldSchema(FieldKinds.sequence, jsonRoot),
61+
FieldSchema.createUnsafe(FieldKinds.sequence, jsonRoot),
6262
);
6363

6464
/**

experimental/dds/tree2/src/domains/nodeKey/nodeKeySchema.ts

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -6,11 +6,11 @@
66
import { assert } from "@fluidframework/core-utils";
77
import { ValueSchema } from "../../core";
88
import {
9-
SchemaBuilder,
109
nodeKeyFieldKey,
1110
FieldKinds,
1211
nodeKeyTreeIdentifier,
1312
SchemaBuilderInternal,
13+
FieldSchema,
1414
} from "../../feature-libraries";
1515

1616
const builder = new SchemaBuilderInternal({ scope: "com.fluidframework.nodeKey" });
@@ -35,7 +35,7 @@ assert(nodeKeyTreeSchema.name === nodeKeyTreeIdentifier, "mismatched identifiers
3535
* @alpha
3636
*/
3737
export const nodeKeyField = {
38-
[nodeKeyFieldKey]: SchemaBuilder.field(FieldKinds.nodeKey, nodeKeyTreeSchema),
38+
[nodeKeyFieldKey]: FieldSchema.create(FieldKinds.nodeKey, [nodeKeyTreeSchema]),
3939
};
4040

4141
/**

experimental/dds/tree2/src/domains/testRecursiveDomain.ts

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -10,7 +10,7 @@
1010
* Currently we do not have tooling in place to test this in our test suite, and exporting these types here is a temporary crutch to aid in diagnosing this issue.
1111
*/
1212

13-
import { AllowedTypes, FieldKinds, SchemaBuilder } from "../feature-libraries";
13+
import { AllowedTypes, FieldKinds, FieldSchema, SchemaBuilder } from "../feature-libraries";
1414
import { areSafelyAssignable, isAny, requireFalse, requireTrue } from "../util";
1515
import * as leaf from "./leafDomain";
1616

@@ -20,7 +20,7 @@ const builder = new SchemaBuilder({ scope: "Test Recursive Domain", libraries: [
2020
* @alpha
2121
*/
2222
export const recursiveStruct = builder.structRecursive("struct", {
23-
recursive: SchemaBuilder.fieldRecursive(FieldKinds.optional, () => recursiveStruct),
23+
recursive: FieldSchema.createUnsafe(FieldKinds.optional, [() => recursiveStruct]),
2424
number: SchemaBuilder.fieldRequired(leaf.number),
2525
});
2626

@@ -34,7 +34,7 @@ fixRecursiveReference(recursiveReference);
3434
* @alpha
3535
*/
3636
export const recursiveStruct2 = builder.struct("struct2", {
37-
recursive: SchemaBuilder.field(FieldKinds.optional, recursiveReference),
37+
recursive: FieldSchema.create(FieldKinds.optional, [recursiveReference]),
3838
number: SchemaBuilder.fieldRequired(leaf.number),
3939
});
4040

experimental/dds/tree2/src/feature-libraries/editable-tree-2/lazyTree.ts

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -231,7 +231,7 @@ export abstract class LazyTree<TSchema extends TreeSchema = TreeSchema>
231231
// Additionally this approach makes it possible for a user to take an EditableTree node, get its parent, check its schema, down cast based on that, then edit that detached field (ex: removing the node in it).
232232
// This MIGHT work properly with existing merge resolution logic (it must keep client in sync and be unable to violate schema), but this either needs robust testing or to be explicitly banned (error before s3ending the op).
233233
// Issues like replacing a node in the a removed sequenced then undoing the remove could easily violate schema if not everything works exactly right!
234-
fieldSchema = new FieldSchema(FieldKinds.sequence, [Any]);
234+
fieldSchema = FieldSchema.create(FieldKinds.sequence, [Any]);
235235
}
236236
} else {
237237
cursor.exitField();
@@ -471,7 +471,7 @@ export abstract class LazyStruct<TSchema extends StructSchema>
471471
assert(field instanceof LazyNodeKeyField, "unexpected node key field");
472472
// TODO: ideally we would do something like this, but that adds dependencies we can't have here:
473473
// assert(
474-
// field.is(new FieldSchema(FieldKinds.nodeKey, [nodeKeyTreeSchema])),
474+
// field.is(FieldSchema.create(FieldKinds.nodeKey, [nodeKeyTreeSchema])),
475475
// "invalid node key field",
476476
// );
477477

experimental/dds/tree2/src/feature-libraries/index.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -157,6 +157,7 @@ export {
157157
bannedFieldNames,
158158
fieldApiPrefixes,
159159
validateStructFieldName,
160+
Unenforced,
160161
} from "./typed-schema";
161162

162163
export { SchemaBuilderBase, SchemaLibrary } from "./schemaBuilderBase";

experimental/dds/tree2/src/feature-libraries/schemaBuilder.ts

Lines changed: 28 additions & 21 deletions
Original file line numberDiff line numberDiff line change
@@ -8,7 +8,14 @@ import { ValueSchema } from "../core";
88
import { Assume, RestrictiveReadonlyRecord, transformObjectMap } from "../util";
99
import { SchemaBuilderBase } from "./schemaBuilderBase";
1010
import { FieldKinds } from "./default-field-kinds";
11-
import { AllowedTypes, TreeSchema, FieldSchema, Any, TypedSchemaCollection } from "./typed-schema";
11+
import {
12+
AllowedTypes,
13+
TreeSchema,
14+
FieldSchema,
15+
Any,
16+
TypedSchemaCollection,
17+
Unenforced,
18+
} from "./typed-schema";
1219
import { FieldKind } from "./modular-schema";
1320

1421
// TODO: tests and examples for this file
@@ -66,14 +73,14 @@ export class SchemaBuilder<
6673
* Same as `struct` but with less type safety and works for recursive objects.
6774
* Reduced type safety is a side effect of a workaround for a TypeScript limitation.
6875
*
69-
* See note on {@link InternalTypedSchemaTypes#RecursiveTreeSchema} for details.
76+
* See {@link Unenforced} for details.
7077
*
7178
* TODO: Make this work with ImplicitFieldSchema.
7279
*/
73-
public structRecursive<Name extends TName, T>(
74-
name: Name,
75-
t: T,
76-
): TreeSchema<`${TScope}.${Name}`, { structFields: T }> {
80+
public structRecursive<
81+
Name extends TName,
82+
const T extends Unenforced<RestrictiveReadonlyRecord<string, ImplicitFieldSchema>>,
83+
>(name: Name, t: T): TreeSchema<`${TScope}.${Name}`, { structFields: T }> {
7784
return this.struct(
7885
name,
7986
t as unknown as RestrictiveReadonlyRecord<string, ImplicitFieldSchema>,
@@ -83,7 +90,7 @@ export class SchemaBuilder<
8390
/**
8491
* Define (and add to this library) a {@link TreeSchema} for a {@link MapNode}.
8592
*/
86-
public map<Name extends TName, T extends ImplicitFieldSchema>(
93+
public map<Name extends TName, const T extends ImplicitFieldSchema>(
8794
name: Name,
8895
fieldSchema: T,
8996
): TreeSchema<`${TScope}.${Name}`, { mapFields: NormalizeField<T, DefaultFieldKind> }> {
@@ -98,11 +105,11 @@ export class SchemaBuilder<
98105
* Same as `map` but with less type safety and works for recursive objects.
99106
* Reduced type safety is a side effect of a workaround for a TypeScript limitation.
100107
*
101-
* See note on {@link InternalTypedSchemaTypes#RecursiveTreeSchema} for details.
108+
* See {@link Unenforced} for details.
102109
*
103110
* TODO: Make this work with ImplicitFieldSchema.
104111
*/
105-
public mapRecursive<Name extends TName, T>(
112+
public mapRecursive<Name extends TName, const T extends Unenforced<ImplicitFieldSchema>>(
106113
name: Name,
107114
t: T,
108115
): TreeSchema<`${TScope}.${Name}`, { mapFields: T }> {
@@ -121,7 +128,7 @@ export class SchemaBuilder<
121128
* TODO: Write and link document outlining field vs node data model and the separation of concerns related to that.
122129
* TODO: Maybe find a better name for this.
123130
*/
124-
public fieldNode<Name extends TName, T extends ImplicitFieldSchema>(
131+
public fieldNode<Name extends TName, const T extends ImplicitFieldSchema>(
125132
name: Name,
126133
fieldSchema: T,
127134
): TreeSchema<
@@ -139,11 +146,11 @@ export class SchemaBuilder<
139146
* Same as `fieldNode` but with less type safety and works for recursive objects.
140147
* Reduced type safety is a side effect of a workaround for a TypeScript limitation.
141148
*
142-
* See note on {@link InternalTypedSchemaTypes#RecursiveTreeSchema} for details.
149+
* See {@link Unenforced} for details.
143150
*
144151
* TODO: Make this work with ImplicitFieldSchema.
145152
*/
146-
public fieldNodeRecursive<Name extends TName, T>(
153+
public fieldNodeRecursive<Name extends TName, const T extends Unenforced<ImplicitFieldSchema>>(
147154
name: Name,
148155
t: T,
149156
): TreeSchema<`${TScope}.${Name}`, { structFields: { [""]: T } }> {
@@ -167,7 +174,7 @@ export class SchemaBuilder<
167174
* TODO: Maybe ban undefined from allowed values here.
168175
* TODO: Decide and document how unwrapping works for non-primitive terminals.
169176
*/
170-
public leaf<Name extends TName, T extends ValueSchema>(
177+
public leaf<Name extends TName, const T extends ValueSchema>(
171178
name: Name,
172179
t: T,
173180
): TreeSchema<`${TScope}.${Name}`, { leafValue: T }> {
@@ -180,10 +187,10 @@ export class SchemaBuilder<
180187
* Define a schema for an {@link OptionalField}.
181188
* Shorthand or passing `FieldKinds.optional` to {@link FieldSchema}.
182189
*/
183-
public static fieldOptional<T extends AllowedTypes>(
190+
public static fieldOptional<const T extends AllowedTypes>(
184191
...allowedTypes: T
185192
): FieldSchema<typeof FieldKinds.optional, T> {
186-
return new FieldSchema(FieldKinds.optional, allowedTypes);
193+
return FieldSchema.create(FieldKinds.optional, allowedTypes);
187194
}
188195

189196
/**
@@ -195,19 +202,19 @@ export class SchemaBuilder<
195202
* - AllowedTypes can be used as a FieldSchema (Or SchemaBuilder takes a default field kind).
196203
* - A TreeSchema can be used as AllowedTypes in the non-polymorphic case.
197204
*/
198-
public static fieldRequired<T extends AllowedTypes>(
205+
public static fieldRequired<const T extends AllowedTypes>(
199206
...allowedTypes: T
200207
): FieldSchema<typeof FieldKinds.required, T> {
201-
return new FieldSchema(FieldKinds.required, allowedTypes);
208+
return FieldSchema.create(FieldKinds.required, allowedTypes);
202209
}
203210

204211
/**
205212
* Define a schema for a {@link Sequence} field.
206213
*/
207-
public static fieldSequence<T extends AllowedTypes>(
214+
public static fieldSequence<const T extends AllowedTypes>(
208215
...t: T
209216
): FieldSchema<typeof FieldKinds.sequence, T> {
210-
return new FieldSchema(FieldKinds.sequence, t);
217+
return FieldSchema.create(FieldKinds.sequence, t);
211218
}
212219

213220
/**
@@ -218,7 +225,7 @@ export class SchemaBuilder<
218225
*
219226
* May only be called once after adding content to builder is complete.
220227
*/
221-
public toDocumentSchema<TSchema extends ImplicitFieldSchema>(
228+
public toDocumentSchema<const TSchema extends ImplicitFieldSchema>(
222229
root: TSchema,
223230
): TypedSchemaCollection<NormalizeField<TSchema, DefaultFieldKind>> {
224231
return this.toDocumentSchemaInternal(normalizeField(root, DefaultFieldKind));
@@ -273,7 +280,7 @@ export function normalizeField<TSchema extends ImplicitFieldSchema, TDefault ext
273280
return schema as NormalizeField<TSchema, TDefault>;
274281
}
275282
const allowedTypes = normalizeAllowedTypes(schema);
276-
return new FieldSchema(defaultKind, allowedTypes) as unknown as NormalizeField<
283+
return FieldSchema.create(defaultKind, allowedTypes) as unknown as NormalizeField<
277284
TSchema,
278285
TDefault
279286
>;

0 commit comments

Comments
 (0)