diff --git a/.changeset/metal-sloths-join.md b/.changeset/metal-sloths-join.md new file mode 100644 index 000000000000..47f9a5f42953 --- /dev/null +++ b/.changeset/metal-sloths-join.md @@ -0,0 +1,27 @@ +--- +"fluid-framework": minor +"@fluidframework/tree": minor +--- +--- +"section": tree +--- + +Disallow some invalid and unsafe ObjectNode field assignments at compile time + +The compile time validation of the type of values assigned to ObjectNode fields is limited by TypeScript's limitations. +Two cases which were actually possible to disallow and should be disallowed for consistency with runtime behavior and similar APIs were being allowed: + +1. [Identifier fields](https://fluidframework.com/docs/api/v2/fluid-framework/schemafactory-class#identifier-property): + Identifier fields are immutable, and setting them produces a runtime error. + This changes fixes them to no longer be typed as assignable. + +2. Fields with non-exact schema: + When non-exact scheme is used for a field (for example the schema is either a schema only allowing numbers or a schema only allowing strings) the field is no longer typed as assignable. + This matches how constructors and implicit node construction work. + For example when a node `Foo` has such an non-exact schema for field `bar`, you can no longer unsafely do `foo.bar = 5` just like how you could already not do `new Foo({bar: 5})`. + +This fix only applies to [`SchemaFactory.object`](https://fluidframework.com/docs/api/v2/fluid-framework/schemafactory-class#object-method). +[`SchemaFactory.objectRecursive`](https://fluidframework.com/docs/api/v2/fluid-framework/schemafactory-class#objectrecursive-method) was unable to be updated to match due to TypeScript limitations on recursive types. + +An `@alpha` API, `customizeSchemaTyping` has been added to allow control over the types generated from schema. +For example code relying on the unsound typing fixed above can restore the behavior using `customizeSchemaTyping`: diff --git a/packages/dds/tree/.vscode/settings.json b/packages/dds/tree/.vscode/settings.json index 28312760378d..a2785069f4af 100644 --- a/packages/dds/tree/.vscode/settings.json +++ b/packages/dds/tree/.vscode/settings.json @@ -19,6 +19,7 @@ "contravariance", "contravariantly", "covariantly", + "Customizer", "deprioritized", "endregion", "fluidframework", diff --git a/packages/dds/tree/api-report/tree.alpha.api.md b/packages/dds/tree/api-report/tree.alpha.api.md index ad81f1ba836e..69242a147231 100644 --- a/packages/dds/tree/api-report/tree.alpha.api.md +++ b/packages/dds/tree/api-report/tree.alpha.api.md @@ -28,11 +28,22 @@ type ApplyKind = { [FieldKind.Identifier]: T; }[Kind]; +// @public +export type ApplyKindAssignment = [Kind] extends [ +FieldKind.Required +] ? T : [Kind] extends [FieldKind.Optional] ? T | undefined : never; + // @public type ApplyKindInput = [ Kind ] extends [FieldKind.Required] ? T : [Kind] extends [FieldKind.Optional] ? T | undefined : [Kind] extends [FieldKind.Identifier] ? DefaultsAreOptional extends true ? T | undefined : T : never; +// @public +export type AssignableTreeFieldFromImplicitField> = [TSchema] extends [FieldSchema] ? ApplyKindAssignment["readWrite"], Kind> : [TSchema] extends [ImplicitAllowedTypes] ? GetTypes["readWrite"] : never; + +// @public +export type AssignableTreeFieldFromImplicitFieldUnsafe> = TSchema extends FieldSchemaUnsafe ? ApplyKindAssignment["readWrite"], Kind> : GetTypesUnsafe["readWrite"]; + // @alpha export function asTreeViewAlpha(view: TreeView): TreeViewAlpha; @@ -60,6 +71,13 @@ export interface CommitMetadata { // @alpha export function comparePersistedSchema(persisted: JsonCompatible, view: ImplicitFieldSchema, options: ICodecOptions, canInitialize: boolean): SchemaCompatibilityStatus; +// @alpha +export namespace Component { + export type ComponentSchemaCollection = (lazyConfiguration: () => TConfig) => LazyArray; + export function composeComponentSchema(allComponents: readonly ComponentSchemaCollection[], lazyConfiguration: () => TConfig): (() => TItem)[]; + export type LazyArray = readonly (() => T)[]; +} + // @alpha export type ConciseTree = Exclude | THandle | ConciseTree[] | { [key: string]: ConciseTree; @@ -80,10 +98,66 @@ export function createSimpleTreeIndex(view: TreeView, indexer: Map, getValue: (nodes: TreeIndexNodes>) => TValue, isKeyValid: (key: TreeIndexKey) => key is TKey, indexableSchema: readonly TSchema[]): SimpleTreeIndex; +// @public +export type CustomizedSchemaTyping = TSchema & { + [CustomizedTyping]: TCustom; +}; + +// @public +export const CustomizedTyping: unique symbol; + +// @public +export type CustomizedTyping = typeof CustomizedTyping; + +// @alpha @sealed +export interface Customizer { + custom>(): CustomizedSchemaTyping[Property] : GetTypes[Property]; + }>; + relaxed(): CustomizedSchemaTyping : TSchema extends AllowedTypes ? TSchema[number] extends LazyItem ? InsertableTypedNode : never : never; + readWrite: TreeNodeFromImplicitAllowedTypes; + output: TreeNodeFromImplicitAllowedTypes; + }>; + simplified>(): CustomizedSchemaTyping; + simplifiedUnrestricted(): CustomizedSchemaTyping; + strict(): CustomizedSchemaTyping>; +} + +// @alpha +export function customizeSchemaTyping(schema: TSchema): Customizer; + +// @public @sealed +export interface CustomTypes { + readonly input: unknown; + readonly output: TreeLeafValue | TreeNode; + readonly readWrite: TreeLeafValue | TreeNode; +} + +// @public +export type DefaultInsertableTreeNodeFromImplicitAllowedTypes = [TSchema] extends [TreeNodeSchema] ? InsertableTypedNode : [TSchema] extends [AllowedTypes] ? InsertableTreeNodeFromAllowedTypes : never; + +// @public +export type DefaultInsertableTreeNodeFromImplicitAllowedTypesUnsafe> = [TSchema] extends [TreeNodeSchemaUnsafe] ? InsertableTypedNodeUnsafe : [TSchema] extends [AllowedTypesUnsafe] ? InsertableTreeNodeFromAllowedTypesUnsafe : never; + // @public @sealed interface DefaultProvider extends ErasedType<"@fluidframework/tree.FieldProvider"> { } +// @public +export type DefaultTreeNodeFromImplicitAllowedTypes = TSchema extends TreeNodeSchema ? NodeFromSchema : TSchema extends AllowedTypes ? NodeFromSchema> : unknown; + +// @public +export type DefaultTreeNodeFromImplicitAllowedTypesUnsafe> = TSchema extends TreeNodeSchemaUnsafe ? NodeFromSchemaUnsafe : TSchema extends AllowedTypesUnsafe ? NodeFromSchemaUnsafe> : unknown; + // @alpha export interface EncodeOptions { readonly useStoredKeys?: boolean; @@ -101,6 +175,9 @@ export function enumFromStrings, true, Record, undefined>; }[Members[number]] : never>; }; +// @alpha +export function evaluateLazySchema(value: LazyItem): T; + // @public type ExtractItemType = Item extends () => infer Result ? Result : Item; @@ -208,6 +285,16 @@ export function getBranch(v // @alpha export function getJsonSchema(schema: ImplicitFieldSchema): JsonTreeSchema; +// @public +export type GetTypes = [TSchema] extends [ +CustomizedSchemaTyping +] ? TCustom : StrictTypes; + +// @public +export type GetTypesUnsafe> = [ +TSchema +] extends [CustomizedSchemaTyping] ? TCustom : StrictTypesUnsafe; + // @alpha export interface ICodecOptions { readonly jsonValidator: JsonValidator; @@ -237,7 +324,7 @@ type _InlineTrick = 0; export type Input = T; // @alpha -export type Insertable = TSchema extends ImplicitAllowedTypes ? InsertableTreeNodeFromImplicitAllowedTypes : InsertableContent; +export type Insertable = InsertableTreeNodeFromImplicitAllowedTypes; // @alpha export type InsertableContent = Unhydrated | FactoryContent; @@ -262,7 +349,7 @@ export type InsertableObjectFromSchemaRecordUnsafe> = [TSchema] extends [FieldSchema] ? ApplyKindInput, Kind, true> : [TSchema] extends [ImplicitAllowedTypes] ? InsertableTreeNodeFromImplicitAllowedTypes : never; +export type InsertableTreeFieldFromImplicitField] ? TSchemaInput : SchemaUnionToIntersection> = [TSchema] extends [FieldSchema] ? ApplyKindInput, Kind, true> : [TSchema] extends [ImplicitAllowedTypes] ? InsertableTreeNodeFromImplicitAllowedTypes : never; // @public export type InsertableTreeFieldFromImplicitFieldUnsafe, TSchema = UnionToIntersection> = [TSchema] extends [FieldSchemaUnsafe] ? ApplyKindInput, Kind, true> : [TSchema] extends [ImplicitAllowedTypes] ? InsertableTreeNodeFromImplicitAllowedTypesUnsafe : never; @@ -280,12 +367,10 @@ LazyItem, ] ? InsertableTypedNodeUnsafe | InsertableTreeNodeFromAllowedTypesUnsafe : never; // @public -export type InsertableTreeNodeFromImplicitAllowedTypes = [ -TSchema -] extends [TreeNodeSchema] ? InsertableTypedNode : [TSchema] extends [AllowedTypes] ? InsertableTreeNodeFromAllowedTypes : never; +export type InsertableTreeNodeFromImplicitAllowedTypes = GetTypes["input"]; // @public -export type InsertableTreeNodeFromImplicitAllowedTypesUnsafe> = [TSchema] extends [TreeNodeSchemaUnsafe] ? InsertableTypedNodeUnsafe : [TSchema] extends [AllowedTypesUnsafe] ? InsertableTreeNodeFromAllowedTypesUnsafe : never; +export type InsertableTreeNodeFromImplicitAllowedTypesUnsafe> = GetTypesUnsafe["input"]; // @public export type InsertableTypedNode> = (T extends TreeNodeSchema ? NodeBuilderData : never) | (T extends TreeNodeSchema ? Unhydrated ? never : NodeFromSchema> : never); @@ -533,12 +618,30 @@ export const noopValidator: JsonValidator; // @public type ObjectFromSchemaRecord> = { - -readonly [Property in keyof T]: Property extends string ? TreeFieldFromImplicitField : unknown; + -readonly [Property in keyof T as [ + AssignableTreeFieldFromImplicitField + ] extends [never | undefined] ? never : Property]: AssignableTreeFieldFromImplicitField; +} & { + readonly [Property in keyof T]: TreeFieldFromImplicitField; }; // @public type ObjectFromSchemaRecordUnsafe>> = { - -readonly [Property in keyof T]: TreeFieldFromImplicitFieldUnsafe; + -readonly [Property in keyof T as [T[Property]] extends [ + CustomizedSchemaTyping + ] ? never : Property]: AssignableTreeFieldFromImplicitFieldUnsafe; +} & { + readonly [Property in keyof T as [T[Property]] extends [ + CustomizedSchemaTyping + ] ? Property : never]: TreeFieldFromImplicitFieldUnsafe; }; // @public @deprecated @@ -734,6 +837,11 @@ export const schemaStatics: { readonly requiredRecursive: (t: T_3, props?: Omit) => FieldSchemaUnsafe; }; +// @public +export type SchemaUnionToIntersection = [T] extends [ +CustomizedSchemaTyping +] ? T : UnionToIntersection; + // @alpha export interface SchemaValidationFunction { check(data: unknown): data is Static; @@ -769,6 +877,26 @@ export function singletonSchema, true, Record, undefined>; +// @public @sealed +export interface StrictTypes, TOutput extends TreeNode | TreeLeafValue = DefaultTreeNodeFromImplicitAllowedTypes> { + // (undocumented) + input: TInput; + // (undocumented) + output: TOutput; + // (undocumented) + readWrite: TInput extends never ? never : TOutput; +} + +// @public +export interface StrictTypesUnsafe, TInput = DefaultInsertableTreeNodeFromImplicitAllowedTypesUnsafe, TOutput = DefaultTreeNodeFromImplicitAllowedTypesUnsafe> { + // (undocumented) + input: TInput; + // (undocumented) + output: TOutput; + // (undocumented) + readWrite: TOutput; +} + // @alpha export type TransactionCallbackStatus = ({ rollback?: false; @@ -964,10 +1092,10 @@ export interface TreeNodeApi { } // @public -export type TreeNodeFromImplicitAllowedTypes = TSchema extends TreeNodeSchema ? NodeFromSchema : TSchema extends AllowedTypes ? NodeFromSchema> : unknown; +export type TreeNodeFromImplicitAllowedTypes = GetTypes["output"]; // @public -type TreeNodeFromImplicitAllowedTypesUnsafe> = TSchema extends TreeNodeSchemaUnsafe ? NodeFromSchemaUnsafe : TSchema extends AllowedTypesUnsafe ? NodeFromSchemaUnsafe> : unknown; +type TreeNodeFromImplicitAllowedTypesUnsafe> = GetTypesUnsafe["output"]; // @public @sealed export type TreeNodeSchema = (TNode extends TreeNode ? TreeNodeSchemaClass : never) | TreeNodeSchemaNonClass; diff --git a/packages/dds/tree/api-report/tree.beta.api.md b/packages/dds/tree/api-report/tree.beta.api.md index bd9eaa384d61..ade91e229e79 100644 --- a/packages/dds/tree/api-report/tree.beta.api.md +++ b/packages/dds/tree/api-report/tree.beta.api.md @@ -17,11 +17,22 @@ type ApplyKind = { [FieldKind.Identifier]: T; }[Kind]; +// @public +export type ApplyKindAssignment = [Kind] extends [ +FieldKind.Required +] ? T : [Kind] extends [FieldKind.Optional] ? T | undefined : never; + // @public type ApplyKindInput = [ Kind ] extends [FieldKind.Required] ? T : [Kind] extends [FieldKind.Optional] ? T | undefined : [Kind] extends [FieldKind.Identifier] ? DefaultsAreOptional extends true ? T | undefined : T : never; +// @public +export type AssignableTreeFieldFromImplicitField> = [TSchema] extends [FieldSchema] ? ApplyKindAssignment["readWrite"], Kind> : [TSchema] extends [ImplicitAllowedTypes] ? GetTypes["readWrite"] : never; + +// @public +export type AssignableTreeFieldFromImplicitFieldUnsafe> = TSchema extends FieldSchemaUnsafe ? ApplyKindAssignment["readWrite"], Kind> : GetTypesUnsafe["readWrite"]; + // @public export enum CommitKind { Default = 0, @@ -35,10 +46,40 @@ export interface CommitMetadata { readonly kind: CommitKind; } +// @public +export type CustomizedSchemaTyping = TSchema & { + [CustomizedTyping]: TCustom; +}; + +// @public +export const CustomizedTyping: unique symbol; + +// @public +export type CustomizedTyping = typeof CustomizedTyping; + +// @public @sealed +export interface CustomTypes { + readonly input: unknown; + readonly output: TreeLeafValue | TreeNode; + readonly readWrite: TreeLeafValue | TreeNode; +} + +// @public +export type DefaultInsertableTreeNodeFromImplicitAllowedTypes = [TSchema] extends [TreeNodeSchema] ? InsertableTypedNode : [TSchema] extends [AllowedTypes] ? InsertableTreeNodeFromAllowedTypes : never; + +// @public +export type DefaultInsertableTreeNodeFromImplicitAllowedTypesUnsafe> = [TSchema] extends [TreeNodeSchemaUnsafe] ? InsertableTypedNodeUnsafe : [TSchema] extends [AllowedTypesUnsafe] ? InsertableTreeNodeFromAllowedTypesUnsafe : never; + // @public @sealed interface DefaultProvider extends ErasedType<"@fluidframework/tree.FieldProvider"> { } +// @public +export type DefaultTreeNodeFromImplicitAllowedTypes = TSchema extends TreeNodeSchema ? NodeFromSchema : TSchema extends AllowedTypes ? NodeFromSchema> : unknown; + +// @public +export type DefaultTreeNodeFromImplicitAllowedTypesUnsafe> = TSchema extends TreeNodeSchemaUnsafe ? NodeFromSchemaUnsafe : TSchema extends AllowedTypesUnsafe ? NodeFromSchemaUnsafe> : unknown; + // @public type ExtractItemType = Item extends () => infer Result ? Result : Item; @@ -97,6 +138,16 @@ type FlexList = readonly LazyItem[]; // @public type FlexListToUnion = ExtractItemType; +// @public +export type GetTypes = [TSchema] extends [ +CustomizedSchemaTyping +] ? TCustom : StrictTypes; + +// @public +export type GetTypesUnsafe> = [ +TSchema +] extends [CustomizedSchemaTyping] ? TCustom : StrictTypesUnsafe; + // @public export type ImplicitAllowedTypes = AllowedTypes | TreeNodeSchema; @@ -124,7 +175,7 @@ export type InsertableObjectFromSchemaRecordUnsafe> = [TSchema] extends [FieldSchema] ? ApplyKindInput, Kind, true> : [TSchema] extends [ImplicitAllowedTypes] ? InsertableTreeNodeFromImplicitAllowedTypes : never; +export type InsertableTreeFieldFromImplicitField] ? TSchemaInput : SchemaUnionToIntersection> = [TSchema] extends [FieldSchema] ? ApplyKindInput, Kind, true> : [TSchema] extends [ImplicitAllowedTypes] ? InsertableTreeNodeFromImplicitAllowedTypes : never; // @public export type InsertableTreeFieldFromImplicitFieldUnsafe, TSchema = UnionToIntersection> = [TSchema] extends [FieldSchemaUnsafe] ? ApplyKindInput, Kind, true> : [TSchema] extends [ImplicitAllowedTypes] ? InsertableTreeNodeFromImplicitAllowedTypesUnsafe : never; @@ -142,12 +193,10 @@ LazyItem, ] ? InsertableTypedNodeUnsafe | InsertableTreeNodeFromAllowedTypesUnsafe : never; // @public -export type InsertableTreeNodeFromImplicitAllowedTypes = [ -TSchema -] extends [TreeNodeSchema] ? InsertableTypedNode : [TSchema] extends [AllowedTypes] ? InsertableTreeNodeFromAllowedTypes : never; +export type InsertableTreeNodeFromImplicitAllowedTypes = GetTypes["input"]; // @public -export type InsertableTreeNodeFromImplicitAllowedTypesUnsafe> = [TSchema] extends [TreeNodeSchemaUnsafe] ? InsertableTypedNodeUnsafe : [TSchema] extends [AllowedTypesUnsafe] ? InsertableTreeNodeFromAllowedTypesUnsafe : never; +export type InsertableTreeNodeFromImplicitAllowedTypesUnsafe> = GetTypesUnsafe["input"]; // @public export type InsertableTypedNode> = (T extends TreeNodeSchema ? NodeBuilderData : never) | (T extends TreeNodeSchema ? Unhydrated ? never : NodeFromSchema> : never); @@ -282,12 +331,30 @@ export interface NodeSchemaOptions { // @public type ObjectFromSchemaRecord> = { - -readonly [Property in keyof T]: Property extends string ? TreeFieldFromImplicitField : unknown; + -readonly [Property in keyof T as [ + AssignableTreeFieldFromImplicitField + ] extends [never | undefined] ? never : Property]: AssignableTreeFieldFromImplicitField; +} & { + readonly [Property in keyof T]: TreeFieldFromImplicitField; }; // @public type ObjectFromSchemaRecordUnsafe>> = { - -readonly [Property in keyof T]: TreeFieldFromImplicitFieldUnsafe; + -readonly [Property in keyof T as [T[Property]] extends [ + CustomizedSchemaTyping + ] ? never : Property]: AssignableTreeFieldFromImplicitFieldUnsafe; +} & { + readonly [Property in keyof T as [T[Property]] extends [ + CustomizedSchemaTyping + ] ? Property : never]: TreeFieldFromImplicitFieldUnsafe; }; // @public @deprecated @@ -429,9 +496,34 @@ export const schemaStatics: { readonly requiredRecursive: (t: T_3, props?: Omit) => FieldSchemaUnsafe; }; +// @public +export type SchemaUnionToIntersection = [T] extends [ +CustomizedSchemaTyping +] ? T : UnionToIntersection; + // @public type ScopedSchemaName = TScope extends undefined ? `${TName}` : `${TScope}.${TName}`; +// @public @sealed +export interface StrictTypes, TOutput extends TreeNode | TreeLeafValue = DefaultTreeNodeFromImplicitAllowedTypes> { + // (undocumented) + input: TInput; + // (undocumented) + output: TOutput; + // (undocumented) + readWrite: TInput extends never ? never : TOutput; +} + +// @public +export interface StrictTypesUnsafe, TInput = DefaultInsertableTreeNodeFromImplicitAllowedTypesUnsafe, TOutput = DefaultTreeNodeFromImplicitAllowedTypesUnsafe> { + // (undocumented) + input: TInput; + // (undocumented) + output: TOutput; + // (undocumented) + readWrite: TOutput; +} + // @public export type TransactionConstraint = NodeInDocumentConstraint; @@ -539,10 +631,10 @@ export interface TreeNodeApi { } // @public -export type TreeNodeFromImplicitAllowedTypes = TSchema extends TreeNodeSchema ? NodeFromSchema : TSchema extends AllowedTypes ? NodeFromSchema> : unknown; +export type TreeNodeFromImplicitAllowedTypes = GetTypes["output"]; // @public -type TreeNodeFromImplicitAllowedTypesUnsafe> = TSchema extends TreeNodeSchemaUnsafe ? NodeFromSchemaUnsafe : TSchema extends AllowedTypesUnsafe ? NodeFromSchemaUnsafe> : unknown; +type TreeNodeFromImplicitAllowedTypesUnsafe> = GetTypesUnsafe["output"]; // @public @sealed export type TreeNodeSchema = (TNode extends TreeNode ? TreeNodeSchemaClass : never) | TreeNodeSchemaNonClass; diff --git a/packages/dds/tree/api-report/tree.legacy.alpha.api.md b/packages/dds/tree/api-report/tree.legacy.alpha.api.md index 78e9288fabc1..356eab39bee4 100644 --- a/packages/dds/tree/api-report/tree.legacy.alpha.api.md +++ b/packages/dds/tree/api-report/tree.legacy.alpha.api.md @@ -17,11 +17,22 @@ type ApplyKind = { [FieldKind.Identifier]: T; }[Kind]; +// @public +export type ApplyKindAssignment = [Kind] extends [ +FieldKind.Required +] ? T : [Kind] extends [FieldKind.Optional] ? T | undefined : never; + // @public type ApplyKindInput = [ Kind ] extends [FieldKind.Required] ? T : [Kind] extends [FieldKind.Optional] ? T | undefined : [Kind] extends [FieldKind.Identifier] ? DefaultsAreOptional extends true ? T | undefined : T : never; +// @public +export type AssignableTreeFieldFromImplicitField> = [TSchema] extends [FieldSchema] ? ApplyKindAssignment["readWrite"], Kind> : [TSchema] extends [ImplicitAllowedTypes] ? GetTypes["readWrite"] : never; + +// @public +export type AssignableTreeFieldFromImplicitFieldUnsafe> = TSchema extends FieldSchemaUnsafe ? ApplyKindAssignment["readWrite"], Kind> : GetTypesUnsafe["readWrite"]; + // @public export enum CommitKind { Default = 0, @@ -35,10 +46,40 @@ export interface CommitMetadata { readonly kind: CommitKind; } +// @public +export type CustomizedSchemaTyping = TSchema & { + [CustomizedTyping]: TCustom; +}; + +// @public +export const CustomizedTyping: unique symbol; + +// @public +export type CustomizedTyping = typeof CustomizedTyping; + +// @public @sealed +export interface CustomTypes { + readonly input: unknown; + readonly output: TreeLeafValue | TreeNode; + readonly readWrite: TreeLeafValue | TreeNode; +} + +// @public +export type DefaultInsertableTreeNodeFromImplicitAllowedTypes = [TSchema] extends [TreeNodeSchema] ? InsertableTypedNode : [TSchema] extends [AllowedTypes] ? InsertableTreeNodeFromAllowedTypes : never; + +// @public +export type DefaultInsertableTreeNodeFromImplicitAllowedTypesUnsafe> = [TSchema] extends [TreeNodeSchemaUnsafe] ? InsertableTypedNodeUnsafe : [TSchema] extends [AllowedTypesUnsafe] ? InsertableTreeNodeFromAllowedTypesUnsafe : never; + // @public @sealed interface DefaultProvider extends ErasedType<"@fluidframework/tree.FieldProvider"> { } +// @public +export type DefaultTreeNodeFromImplicitAllowedTypes = TSchema extends TreeNodeSchema ? NodeFromSchema : TSchema extends AllowedTypes ? NodeFromSchema> : unknown; + +// @public +export type DefaultTreeNodeFromImplicitAllowedTypesUnsafe> = TSchema extends TreeNodeSchemaUnsafe ? NodeFromSchemaUnsafe : TSchema extends AllowedTypesUnsafe ? NodeFromSchemaUnsafe> : unknown; + // @public type ExtractItemType = Item extends () => infer Result ? Result : Item; @@ -97,6 +138,16 @@ type FlexList = readonly LazyItem[]; // @public type FlexListToUnion = ExtractItemType; +// @public +export type GetTypes = [TSchema] extends [ +CustomizedSchemaTyping +] ? TCustom : StrictTypes; + +// @public +export type GetTypesUnsafe> = [ +TSchema +] extends [CustomizedSchemaTyping] ? TCustom : StrictTypesUnsafe; + // @public export type ImplicitAllowedTypes = AllowedTypes | TreeNodeSchema; @@ -124,7 +175,7 @@ export type InsertableObjectFromSchemaRecordUnsafe> = [TSchema] extends [FieldSchema] ? ApplyKindInput, Kind, true> : [TSchema] extends [ImplicitAllowedTypes] ? InsertableTreeNodeFromImplicitAllowedTypes : never; +export type InsertableTreeFieldFromImplicitField] ? TSchemaInput : SchemaUnionToIntersection> = [TSchema] extends [FieldSchema] ? ApplyKindInput, Kind, true> : [TSchema] extends [ImplicitAllowedTypes] ? InsertableTreeNodeFromImplicitAllowedTypes : never; // @public export type InsertableTreeFieldFromImplicitFieldUnsafe, TSchema = UnionToIntersection> = [TSchema] extends [FieldSchemaUnsafe] ? ApplyKindInput, Kind, true> : [TSchema] extends [ImplicitAllowedTypes] ? InsertableTreeNodeFromImplicitAllowedTypesUnsafe : never; @@ -142,12 +193,10 @@ LazyItem, ] ? InsertableTypedNodeUnsafe | InsertableTreeNodeFromAllowedTypesUnsafe : never; // @public -export type InsertableTreeNodeFromImplicitAllowedTypes = [ -TSchema -] extends [TreeNodeSchema] ? InsertableTypedNode : [TSchema] extends [AllowedTypes] ? InsertableTreeNodeFromAllowedTypes : never; +export type InsertableTreeNodeFromImplicitAllowedTypes = GetTypes["input"]; // @public -export type InsertableTreeNodeFromImplicitAllowedTypesUnsafe> = [TSchema] extends [TreeNodeSchemaUnsafe] ? InsertableTypedNodeUnsafe : [TSchema] extends [AllowedTypesUnsafe] ? InsertableTreeNodeFromAllowedTypesUnsafe : never; +export type InsertableTreeNodeFromImplicitAllowedTypesUnsafe> = GetTypesUnsafe["input"]; // @public export type InsertableTypedNode> = (T extends TreeNodeSchema ? NodeBuilderData : never) | (T extends TreeNodeSchema ? Unhydrated ? never : NodeFromSchema> : never); @@ -277,12 +326,30 @@ export interface NodeSchemaOptions { // @public type ObjectFromSchemaRecord> = { - -readonly [Property in keyof T]: Property extends string ? TreeFieldFromImplicitField : unknown; + -readonly [Property in keyof T as [ + AssignableTreeFieldFromImplicitField + ] extends [never | undefined] ? never : Property]: AssignableTreeFieldFromImplicitField; +} & { + readonly [Property in keyof T]: TreeFieldFromImplicitField; }; // @public type ObjectFromSchemaRecordUnsafe>> = { - -readonly [Property in keyof T]: TreeFieldFromImplicitFieldUnsafe; + -readonly [Property in keyof T as [T[Property]] extends [ + CustomizedSchemaTyping + ] ? never : Property]: AssignableTreeFieldFromImplicitFieldUnsafe; +} & { + readonly [Property in keyof T as [T[Property]] extends [ + CustomizedSchemaTyping + ] ? Property : never]: TreeFieldFromImplicitFieldUnsafe; }; // @public @deprecated @@ -424,6 +491,11 @@ export const schemaStatics: { readonly requiredRecursive: (t: T_3, props?: Omit) => FieldSchemaUnsafe; }; +// @public +export type SchemaUnionToIntersection = [T] extends [ +CustomizedSchemaTyping +] ? T : UnionToIntersection; + // @public type ScopedSchemaName = TScope extends undefined ? `${TName}` : `${TScope}.${TName}`; @@ -436,6 +508,26 @@ export const SharedTreeAttributes: IChannelAttributes; // @alpha export const SharedTreeFactoryType = "https://graph.microsoft.com/types/tree"; +// @public @sealed +export interface StrictTypes, TOutput extends TreeNode | TreeLeafValue = DefaultTreeNodeFromImplicitAllowedTypes> { + // (undocumented) + input: TInput; + // (undocumented) + output: TOutput; + // (undocumented) + readWrite: TInput extends never ? never : TOutput; +} + +// @public +export interface StrictTypesUnsafe, TInput = DefaultInsertableTreeNodeFromImplicitAllowedTypesUnsafe, TOutput = DefaultTreeNodeFromImplicitAllowedTypesUnsafe> { + // (undocumented) + input: TInput; + // (undocumented) + output: TOutput; + // (undocumented) + readWrite: TOutput; +} + // @public export type TransactionConstraint = NodeInDocumentConstraint; @@ -532,10 +624,10 @@ export interface TreeNodeApi { } // @public -export type TreeNodeFromImplicitAllowedTypes = TSchema extends TreeNodeSchema ? NodeFromSchema : TSchema extends AllowedTypes ? NodeFromSchema> : unknown; +export type TreeNodeFromImplicitAllowedTypes = GetTypes["output"]; // @public -type TreeNodeFromImplicitAllowedTypesUnsafe> = TSchema extends TreeNodeSchemaUnsafe ? NodeFromSchemaUnsafe : TSchema extends AllowedTypesUnsafe ? NodeFromSchemaUnsafe> : unknown; +type TreeNodeFromImplicitAllowedTypesUnsafe> = GetTypesUnsafe["output"]; // @public @sealed export type TreeNodeSchema = (TNode extends TreeNode ? TreeNodeSchemaClass : never) | TreeNodeSchemaNonClass; diff --git a/packages/dds/tree/api-report/tree.legacy.public.api.md b/packages/dds/tree/api-report/tree.legacy.public.api.md index 887541fa6fed..a9d132791184 100644 --- a/packages/dds/tree/api-report/tree.legacy.public.api.md +++ b/packages/dds/tree/api-report/tree.legacy.public.api.md @@ -17,11 +17,22 @@ type ApplyKind = { [FieldKind.Identifier]: T; }[Kind]; +// @public +export type ApplyKindAssignment = [Kind] extends [ +FieldKind.Required +] ? T : [Kind] extends [FieldKind.Optional] ? T | undefined : never; + // @public type ApplyKindInput = [ Kind ] extends [FieldKind.Required] ? T : [Kind] extends [FieldKind.Optional] ? T | undefined : [Kind] extends [FieldKind.Identifier] ? DefaultsAreOptional extends true ? T | undefined : T : never; +// @public +export type AssignableTreeFieldFromImplicitField> = [TSchema] extends [FieldSchema] ? ApplyKindAssignment["readWrite"], Kind> : [TSchema] extends [ImplicitAllowedTypes] ? GetTypes["readWrite"] : never; + +// @public +export type AssignableTreeFieldFromImplicitFieldUnsafe> = TSchema extends FieldSchemaUnsafe ? ApplyKindAssignment["readWrite"], Kind> : GetTypesUnsafe["readWrite"]; + // @public export enum CommitKind { Default = 0, @@ -35,10 +46,40 @@ export interface CommitMetadata { readonly kind: CommitKind; } +// @public +export type CustomizedSchemaTyping = TSchema & { + [CustomizedTyping]: TCustom; +}; + +// @public +export const CustomizedTyping: unique symbol; + +// @public +export type CustomizedTyping = typeof CustomizedTyping; + +// @public @sealed +export interface CustomTypes { + readonly input: unknown; + readonly output: TreeLeafValue | TreeNode; + readonly readWrite: TreeLeafValue | TreeNode; +} + +// @public +export type DefaultInsertableTreeNodeFromImplicitAllowedTypes = [TSchema] extends [TreeNodeSchema] ? InsertableTypedNode : [TSchema] extends [AllowedTypes] ? InsertableTreeNodeFromAllowedTypes : never; + +// @public +export type DefaultInsertableTreeNodeFromImplicitAllowedTypesUnsafe> = [TSchema] extends [TreeNodeSchemaUnsafe] ? InsertableTypedNodeUnsafe : [TSchema] extends [AllowedTypesUnsafe] ? InsertableTreeNodeFromAllowedTypesUnsafe : never; + // @public @sealed interface DefaultProvider extends ErasedType<"@fluidframework/tree.FieldProvider"> { } +// @public +export type DefaultTreeNodeFromImplicitAllowedTypes = TSchema extends TreeNodeSchema ? NodeFromSchema : TSchema extends AllowedTypes ? NodeFromSchema> : unknown; + +// @public +export type DefaultTreeNodeFromImplicitAllowedTypesUnsafe> = TSchema extends TreeNodeSchemaUnsafe ? NodeFromSchemaUnsafe : TSchema extends AllowedTypesUnsafe ? NodeFromSchemaUnsafe> : unknown; + // @public type ExtractItemType = Item extends () => infer Result ? Result : Item; @@ -97,6 +138,16 @@ type FlexList = readonly LazyItem[]; // @public type FlexListToUnion = ExtractItemType; +// @public +export type GetTypes = [TSchema] extends [ +CustomizedSchemaTyping +] ? TCustom : StrictTypes; + +// @public +export type GetTypesUnsafe> = [ +TSchema +] extends [CustomizedSchemaTyping] ? TCustom : StrictTypesUnsafe; + // @public export type ImplicitAllowedTypes = AllowedTypes | TreeNodeSchema; @@ -124,7 +175,7 @@ export type InsertableObjectFromSchemaRecordUnsafe> = [TSchema] extends [FieldSchema] ? ApplyKindInput, Kind, true> : [TSchema] extends [ImplicitAllowedTypes] ? InsertableTreeNodeFromImplicitAllowedTypes : never; +export type InsertableTreeFieldFromImplicitField] ? TSchemaInput : SchemaUnionToIntersection> = [TSchema] extends [FieldSchema] ? ApplyKindInput, Kind, true> : [TSchema] extends [ImplicitAllowedTypes] ? InsertableTreeNodeFromImplicitAllowedTypes : never; // @public export type InsertableTreeFieldFromImplicitFieldUnsafe, TSchema = UnionToIntersection> = [TSchema] extends [FieldSchemaUnsafe] ? ApplyKindInput, Kind, true> : [TSchema] extends [ImplicitAllowedTypes] ? InsertableTreeNodeFromImplicitAllowedTypesUnsafe : never; @@ -142,12 +193,10 @@ LazyItem, ] ? InsertableTypedNodeUnsafe | InsertableTreeNodeFromAllowedTypesUnsafe : never; // @public -export type InsertableTreeNodeFromImplicitAllowedTypes = [ -TSchema -] extends [TreeNodeSchema] ? InsertableTypedNode : [TSchema] extends [AllowedTypes] ? InsertableTreeNodeFromAllowedTypes : never; +export type InsertableTreeNodeFromImplicitAllowedTypes = GetTypes["input"]; // @public -export type InsertableTreeNodeFromImplicitAllowedTypesUnsafe> = [TSchema] extends [TreeNodeSchemaUnsafe] ? InsertableTypedNodeUnsafe : [TSchema] extends [AllowedTypesUnsafe] ? InsertableTreeNodeFromAllowedTypesUnsafe : never; +export type InsertableTreeNodeFromImplicitAllowedTypesUnsafe> = GetTypesUnsafe["input"]; // @public export type InsertableTypedNode> = (T extends TreeNodeSchema ? NodeBuilderData : never) | (T extends TreeNodeSchema ? Unhydrated ? never : NodeFromSchema> : never); @@ -277,12 +326,30 @@ export interface NodeSchemaOptions { // @public type ObjectFromSchemaRecord> = { - -readonly [Property in keyof T]: Property extends string ? TreeFieldFromImplicitField : unknown; + -readonly [Property in keyof T as [ + AssignableTreeFieldFromImplicitField + ] extends [never | undefined] ? never : Property]: AssignableTreeFieldFromImplicitField; +} & { + readonly [Property in keyof T]: TreeFieldFromImplicitField; }; // @public type ObjectFromSchemaRecordUnsafe>> = { - -readonly [Property in keyof T]: TreeFieldFromImplicitFieldUnsafe; + -readonly [Property in keyof T as [T[Property]] extends [ + CustomizedSchemaTyping + ] ? never : Property]: AssignableTreeFieldFromImplicitFieldUnsafe; +} & { + readonly [Property in keyof T as [T[Property]] extends [ + CustomizedSchemaTyping + ] ? Property : never]: TreeFieldFromImplicitFieldUnsafe; }; // @public @deprecated @@ -424,9 +491,34 @@ export const schemaStatics: { readonly requiredRecursive: (t: T_3, props?: Omit) => FieldSchemaUnsafe; }; +// @public +export type SchemaUnionToIntersection = [T] extends [ +CustomizedSchemaTyping +] ? T : UnionToIntersection; + // @public type ScopedSchemaName = TScope extends undefined ? `${TName}` : `${TScope}.${TName}`; +// @public @sealed +export interface StrictTypes, TOutput extends TreeNode | TreeLeafValue = DefaultTreeNodeFromImplicitAllowedTypes> { + // (undocumented) + input: TInput; + // (undocumented) + output: TOutput; + // (undocumented) + readWrite: TInput extends never ? never : TOutput; +} + +// @public +export interface StrictTypesUnsafe, TInput = DefaultInsertableTreeNodeFromImplicitAllowedTypesUnsafe, TOutput = DefaultTreeNodeFromImplicitAllowedTypesUnsafe> { + // (undocumented) + input: TInput; + // (undocumented) + output: TOutput; + // (undocumented) + readWrite: TOutput; +} + // @public export type TransactionConstraint = NodeInDocumentConstraint; @@ -523,10 +615,10 @@ export interface TreeNodeApi { } // @public -export type TreeNodeFromImplicitAllowedTypes = TSchema extends TreeNodeSchema ? NodeFromSchema : TSchema extends AllowedTypes ? NodeFromSchema> : unknown; +export type TreeNodeFromImplicitAllowedTypes = GetTypes["output"]; // @public -type TreeNodeFromImplicitAllowedTypesUnsafe> = TSchema extends TreeNodeSchemaUnsafe ? NodeFromSchemaUnsafe : TSchema extends AllowedTypesUnsafe ? NodeFromSchemaUnsafe> : unknown; +type TreeNodeFromImplicitAllowedTypesUnsafe> = GetTypesUnsafe["output"]; // @public @sealed export type TreeNodeSchema = (TNode extends TreeNode ? TreeNodeSchemaClass : never) | TreeNodeSchemaNonClass; diff --git a/packages/dds/tree/api-report/tree.public.api.md b/packages/dds/tree/api-report/tree.public.api.md index 887541fa6fed..a9d132791184 100644 --- a/packages/dds/tree/api-report/tree.public.api.md +++ b/packages/dds/tree/api-report/tree.public.api.md @@ -17,11 +17,22 @@ type ApplyKind = { [FieldKind.Identifier]: T; }[Kind]; +// @public +export type ApplyKindAssignment = [Kind] extends [ +FieldKind.Required +] ? T : [Kind] extends [FieldKind.Optional] ? T | undefined : never; + // @public type ApplyKindInput = [ Kind ] extends [FieldKind.Required] ? T : [Kind] extends [FieldKind.Optional] ? T | undefined : [Kind] extends [FieldKind.Identifier] ? DefaultsAreOptional extends true ? T | undefined : T : never; +// @public +export type AssignableTreeFieldFromImplicitField> = [TSchema] extends [FieldSchema] ? ApplyKindAssignment["readWrite"], Kind> : [TSchema] extends [ImplicitAllowedTypes] ? GetTypes["readWrite"] : never; + +// @public +export type AssignableTreeFieldFromImplicitFieldUnsafe> = TSchema extends FieldSchemaUnsafe ? ApplyKindAssignment["readWrite"], Kind> : GetTypesUnsafe["readWrite"]; + // @public export enum CommitKind { Default = 0, @@ -35,10 +46,40 @@ export interface CommitMetadata { readonly kind: CommitKind; } +// @public +export type CustomizedSchemaTyping = TSchema & { + [CustomizedTyping]: TCustom; +}; + +// @public +export const CustomizedTyping: unique symbol; + +// @public +export type CustomizedTyping = typeof CustomizedTyping; + +// @public @sealed +export interface CustomTypes { + readonly input: unknown; + readonly output: TreeLeafValue | TreeNode; + readonly readWrite: TreeLeafValue | TreeNode; +} + +// @public +export type DefaultInsertableTreeNodeFromImplicitAllowedTypes = [TSchema] extends [TreeNodeSchema] ? InsertableTypedNode : [TSchema] extends [AllowedTypes] ? InsertableTreeNodeFromAllowedTypes : never; + +// @public +export type DefaultInsertableTreeNodeFromImplicitAllowedTypesUnsafe> = [TSchema] extends [TreeNodeSchemaUnsafe] ? InsertableTypedNodeUnsafe : [TSchema] extends [AllowedTypesUnsafe] ? InsertableTreeNodeFromAllowedTypesUnsafe : never; + // @public @sealed interface DefaultProvider extends ErasedType<"@fluidframework/tree.FieldProvider"> { } +// @public +export type DefaultTreeNodeFromImplicitAllowedTypes = TSchema extends TreeNodeSchema ? NodeFromSchema : TSchema extends AllowedTypes ? NodeFromSchema> : unknown; + +// @public +export type DefaultTreeNodeFromImplicitAllowedTypesUnsafe> = TSchema extends TreeNodeSchemaUnsafe ? NodeFromSchemaUnsafe : TSchema extends AllowedTypesUnsafe ? NodeFromSchemaUnsafe> : unknown; + // @public type ExtractItemType = Item extends () => infer Result ? Result : Item; @@ -97,6 +138,16 @@ type FlexList = readonly LazyItem[]; // @public type FlexListToUnion = ExtractItemType; +// @public +export type GetTypes = [TSchema] extends [ +CustomizedSchemaTyping +] ? TCustom : StrictTypes; + +// @public +export type GetTypesUnsafe> = [ +TSchema +] extends [CustomizedSchemaTyping] ? TCustom : StrictTypesUnsafe; + // @public export type ImplicitAllowedTypes = AllowedTypes | TreeNodeSchema; @@ -124,7 +175,7 @@ export type InsertableObjectFromSchemaRecordUnsafe> = [TSchema] extends [FieldSchema] ? ApplyKindInput, Kind, true> : [TSchema] extends [ImplicitAllowedTypes] ? InsertableTreeNodeFromImplicitAllowedTypes : never; +export type InsertableTreeFieldFromImplicitField] ? TSchemaInput : SchemaUnionToIntersection> = [TSchema] extends [FieldSchema] ? ApplyKindInput, Kind, true> : [TSchema] extends [ImplicitAllowedTypes] ? InsertableTreeNodeFromImplicitAllowedTypes : never; // @public export type InsertableTreeFieldFromImplicitFieldUnsafe, TSchema = UnionToIntersection> = [TSchema] extends [FieldSchemaUnsafe] ? ApplyKindInput, Kind, true> : [TSchema] extends [ImplicitAllowedTypes] ? InsertableTreeNodeFromImplicitAllowedTypesUnsafe : never; @@ -142,12 +193,10 @@ LazyItem, ] ? InsertableTypedNodeUnsafe | InsertableTreeNodeFromAllowedTypesUnsafe : never; // @public -export type InsertableTreeNodeFromImplicitAllowedTypes = [ -TSchema -] extends [TreeNodeSchema] ? InsertableTypedNode : [TSchema] extends [AllowedTypes] ? InsertableTreeNodeFromAllowedTypes : never; +export type InsertableTreeNodeFromImplicitAllowedTypes = GetTypes["input"]; // @public -export type InsertableTreeNodeFromImplicitAllowedTypesUnsafe> = [TSchema] extends [TreeNodeSchemaUnsafe] ? InsertableTypedNodeUnsafe : [TSchema] extends [AllowedTypesUnsafe] ? InsertableTreeNodeFromAllowedTypesUnsafe : never; +export type InsertableTreeNodeFromImplicitAllowedTypesUnsafe> = GetTypesUnsafe["input"]; // @public export type InsertableTypedNode> = (T extends TreeNodeSchema ? NodeBuilderData : never) | (T extends TreeNodeSchema ? Unhydrated ? never : NodeFromSchema> : never); @@ -277,12 +326,30 @@ export interface NodeSchemaOptions { // @public type ObjectFromSchemaRecord> = { - -readonly [Property in keyof T]: Property extends string ? TreeFieldFromImplicitField : unknown; + -readonly [Property in keyof T as [ + AssignableTreeFieldFromImplicitField + ] extends [never | undefined] ? never : Property]: AssignableTreeFieldFromImplicitField; +} & { + readonly [Property in keyof T]: TreeFieldFromImplicitField; }; // @public type ObjectFromSchemaRecordUnsafe>> = { - -readonly [Property in keyof T]: TreeFieldFromImplicitFieldUnsafe; + -readonly [Property in keyof T as [T[Property]] extends [ + CustomizedSchemaTyping + ] ? never : Property]: AssignableTreeFieldFromImplicitFieldUnsafe; +} & { + readonly [Property in keyof T as [T[Property]] extends [ + CustomizedSchemaTyping + ] ? Property : never]: TreeFieldFromImplicitFieldUnsafe; }; // @public @deprecated @@ -424,9 +491,34 @@ export const schemaStatics: { readonly requiredRecursive: (t: T_3, props?: Omit) => FieldSchemaUnsafe; }; +// @public +export type SchemaUnionToIntersection = [T] extends [ +CustomizedSchemaTyping +] ? T : UnionToIntersection; + // @public type ScopedSchemaName = TScope extends undefined ? `${TName}` : `${TScope}.${TName}`; +// @public @sealed +export interface StrictTypes, TOutput extends TreeNode | TreeLeafValue = DefaultTreeNodeFromImplicitAllowedTypes> { + // (undocumented) + input: TInput; + // (undocumented) + output: TOutput; + // (undocumented) + readWrite: TInput extends never ? never : TOutput; +} + +// @public +export interface StrictTypesUnsafe, TInput = DefaultInsertableTreeNodeFromImplicitAllowedTypesUnsafe, TOutput = DefaultTreeNodeFromImplicitAllowedTypesUnsafe> { + // (undocumented) + input: TInput; + // (undocumented) + output: TOutput; + // (undocumented) + readWrite: TOutput; +} + // @public export type TransactionConstraint = NodeInDocumentConstraint; @@ -523,10 +615,10 @@ export interface TreeNodeApi { } // @public -export type TreeNodeFromImplicitAllowedTypes = TSchema extends TreeNodeSchema ? NodeFromSchema : TSchema extends AllowedTypes ? NodeFromSchema> : unknown; +export type TreeNodeFromImplicitAllowedTypes = GetTypes["output"]; // @public -type TreeNodeFromImplicitAllowedTypesUnsafe> = TSchema extends TreeNodeSchemaUnsafe ? NodeFromSchemaUnsafe : TSchema extends AllowedTypesUnsafe ? NodeFromSchemaUnsafe> : unknown; +type TreeNodeFromImplicitAllowedTypesUnsafe> = GetTypesUnsafe["output"]; // @public @sealed export type TreeNodeSchema = (TNode extends TreeNode ? TreeNodeSchemaClass : never) | TreeNodeSchemaNonClass; diff --git a/packages/dds/tree/package.json b/packages/dds/tree/package.json index 097f625b8caf..85ce3b0d3ba1 100644 --- a/packages/dds/tree/package.json +++ b/packages/dds/tree/package.json @@ -227,7 +227,11 @@ } }, "typeValidation": { - "broken": {}, + "broken": { + "TypeAlias_InsertableTreeNodeFromImplicitAllowedTypes": { + "backCompat": false + } + }, "entrypoint": "public" } } diff --git a/packages/dds/tree/src/index.ts b/packages/dds/tree/src/index.ts index ba59f9482a5f..dcf29c15a6e6 100644 --- a/packages/dds/tree/src/index.ts +++ b/packages/dds/tree/src/index.ts @@ -204,6 +204,23 @@ export { asTreeViewAlpha, type NodeSchemaOptions, type NodeSchemaMetadata, + type AssignableTreeFieldFromImplicitField, + type ApplyKindAssignment, + type DefaultTreeNodeFromImplicitAllowedTypes, + type Customizer, + type GetTypes, + type StrictTypes, + type CustomTypes, + type CustomizedSchemaTyping, + CustomizedTyping, + type DefaultInsertableTreeNodeFromImplicitAllowedTypes, + customizeSchemaTyping, + type GetTypesUnsafe, + type DefaultInsertableTreeNodeFromImplicitAllowedTypesUnsafe, + type DefaultTreeNodeFromImplicitAllowedTypesUnsafe, + type StrictTypesUnsafe, + type AssignableTreeFieldFromImplicitFieldUnsafe, + type SchemaUnionToIntersection, type schemaStatics, type ITreeAlpha, type TransactionConstraint, @@ -216,6 +233,8 @@ export { type TransactionResultSuccess, type TransactionResultFailed, rollback, + evaluateLazySchema, + Component, } from "./simple-tree/index.js"; export { SharedTree, diff --git a/packages/dds/tree/src/simple-tree/api/component.ts b/packages/dds/tree/src/simple-tree/api/component.ts new file mode 100644 index 000000000000..c33413ab5b39 --- /dev/null +++ b/packages/dds/tree/src/simple-tree/api/component.ts @@ -0,0 +1,42 @@ +/*! + * Copyright (c) Microsoft Corporation and contributors. All rights reserved. + * Licensed under the MIT License. + */ + +/** + * Utilities for helping implement various application component design patterns. + * @alpha + */ +export namespace Component { + /** + * Function which takes in a lazy configuration and returns a collection of schema types. + * @remarks + * This allows the schema to reference items from the configuration, which could include themselves recursively. + * @alpha + */ + export type ComponentSchemaCollection = ( + lazyConfiguration: () => TConfig, + ) => LazyArray; + + /** + * {@link AllowedTypes} where all of the allowed types' schema implement `T` and are lazy. + * @alpha + */ + export type LazyArray = readonly (() => T)[]; + + /** + * Combine multiple {@link Component.ComponentSchemaCollection}s into a single {@link AllowedTypes} array. + * @remarks + * + * @alpha + */ + export function composeComponentSchema( + allComponents: readonly ComponentSchemaCollection[], + lazyConfiguration: () => TConfig, + ): (() => TItem)[] { + const itemTypes = allComponents.flatMap( + (component): LazyArray => component(lazyConfiguration), + ); + return itemTypes; + } +} diff --git a/packages/dds/tree/src/simple-tree/api/index.ts b/packages/dds/tree/src/simple-tree/api/index.ts index e531037fa106..9351da973139 100644 --- a/packages/dds/tree/src/simple-tree/api/index.ts +++ b/packages/dds/tree/src/simple-tree/api/index.ts @@ -86,6 +86,11 @@ export type { AllowedTypesUnsafe, TreeNodeSchemaNonClassUnsafe, InsertableTreeNodeFromAllowedTypesUnsafe, + GetTypesUnsafe, + DefaultInsertableTreeNodeFromImplicitAllowedTypesUnsafe, + DefaultTreeNodeFromImplicitAllowedTypesUnsafe, + StrictTypesUnsafe, + AssignableTreeFieldFromImplicitFieldUnsafe, } from "./typesUnsafe.js"; export { @@ -133,6 +138,8 @@ export { rollback, } from "./transactionTypes.js"; +export { Component } from "./component.js"; + // Exporting the schema (RecursiveObject) to test that recursive types are working correctly. // These are `@internal` so they can't be included in the `InternalClassTreeTypes` due to https://github.com/microsoft/rushstack/issues/3639 export { diff --git a/packages/dds/tree/src/simple-tree/api/schemaFactory.ts b/packages/dds/tree/src/simple-tree/api/schemaFactory.ts index 0a7ead94e5f3..6a82577b112d 100644 --- a/packages/dds/tree/src/simple-tree/api/schemaFactory.ts +++ b/packages/dds/tree/src/simple-tree/api/schemaFactory.ts @@ -8,7 +8,6 @@ import { assert, unreachableCase } from "@fluidframework/core-utils/internal"; // which degrades the API-Extractor report quality since API-Extractor can not tell the inline import is the same as the non-inline one. // eslint-disable-next-line unused-imports/no-unused-imports import type { IFluidHandle as _dummyImport } from "@fluidframework/core-interfaces"; -import { UsageError } from "@fluidframework/telemetry-utils/internal"; import { isFluidHandle } from "@fluidframework/runtime-utils/internal"; import type { TreeValue } from "../../core/index.js"; @@ -25,7 +24,6 @@ import type { TreeAlpha } from "../../shared-tree/index.js"; import { booleanSchema, handleSchema, - LeafNodeSchema, nullSchema, numberSchema, stringSchema, @@ -41,8 +39,8 @@ import { type DefaultProvider, getDefaultProvider, type NodeSchemaOptions, + markSchemaMostDerived, } from "../schemaTypes.js"; -import { inPrototypeChain } from "../core/index.js"; import type { NodeKind, WithType, @@ -76,7 +74,6 @@ import type { Unenforced, } from "./typesUnsafe.js"; import { createFieldSchemaUnsafe } from "./schemaFactoryRecursive.js"; -import { TreeNodeValid } from "../treeNodeValid.js"; import { isLazy } from "../flexList.js"; /** @@ -1070,27 +1067,3 @@ export function structuralName( } return `${collectionName}<${inner}>`; } - -/** - * Indicates that a schema is the "most derived" version which is allowed to be used, see {@link MostDerivedData}. - * Calling helps with error messages about invalid schema usage (using more than one type from single schema factor produced type, - * and thus calling this for one than one subclass). - * @remarks - * Helper for invoking {@link TreeNodeValid.markMostDerived} for any {@link TreeNodeSchema} if it needed. - */ -export function markSchemaMostDerived(schema: TreeNodeSchema): void { - if (schema instanceof LeafNodeSchema) { - return; - } - - if (!inPrototypeChain(schema, TreeNodeValid)) { - // Use JSON.stringify to quote and escape identifier string. - throw new UsageError( - `Schema for ${JSON.stringify( - schema.identifier, - )} does not extend a SchemaFactory generated class. This is invalid.`, - ); - } - - (schema as typeof TreeNodeValid & TreeNodeSchema).markMostDerived(); -} diff --git a/packages/dds/tree/src/simple-tree/api/tree.ts b/packages/dds/tree/src/simple-tree/api/tree.ts index 349026f7bee7..2ca74cc3171c 100644 --- a/packages/dds/tree/src/simple-tree/api/tree.ts +++ b/packages/dds/tree/src/simple-tree/api/tree.ts @@ -27,13 +27,13 @@ import { type TreeFieldFromImplicitField, type UnsafeUnknownSchema, FieldKind, + markSchemaMostDerived, } from "../schemaTypes.js"; import { NodeKind, type TreeNodeSchema } from "../core/index.js"; import { toStoredSchema } from "../toStoredSchema.js"; import { LeafNodeSchema } from "../leafNodeSchema.js"; import { assert } from "@fluidframework/core-utils/internal"; import { isObjectNodeSchema, type ObjectNodeSchema } from "../objectNodeTypes.js"; -import { markSchemaMostDerived } from "./schemaFactory.js"; import { fail, getOrCreate } from "../../util/index.js"; import type { MakeNominal } from "../../util/index.js"; import { walkFieldSchema } from "../walkFieldSchema.js"; @@ -280,7 +280,7 @@ export class TreeViewConfiguration< // This ensures if multiple schema extending the same schema factory generated class are present (or have been constructed, or get constructed in the future), // an error is reported. - node: markSchemaMostDerived, + node: (schema) => markSchemaMostDerived(schema, true), allowedTypes(types): void { if (config.preventAmbiguity) { checkUnion(types, ambiguityErrors); diff --git a/packages/dds/tree/src/simple-tree/api/treeNodeApi.ts b/packages/dds/tree/src/simple-tree/api/treeNodeApi.ts index 9896e0fbc16c..6d3a8ea1e413 100644 --- a/packages/dds/tree/src/simple-tree/api/treeNodeApi.ts +++ b/packages/dds/tree/src/simple-tree/api/treeNodeApi.ts @@ -7,13 +7,14 @@ import { assert, oob } from "@fluidframework/core-utils/internal"; import { EmptyKey, rootFieldKey } from "../../core/index.js"; import { type TreeStatus, isTreeValue, FieldKinds } from "../../feature-libraries/index.js"; -import { fail, extractFromOpaque, isReadonlyArray } from "../../util/index.js"; +import { fail, extractFromOpaque } from "../../util/index.js"; import { type TreeLeafValue, type ImplicitFieldSchema, FieldSchema, type ImplicitAllowedTypes, type TreeNodeFromImplicitAllowedTypes, + normalizeAllowedTypes, } from "../schemaTypes.js"; import { booleanSchema, @@ -39,8 +40,6 @@ import { getOrCreateInnerNode, } from "../core/index.js"; import { isObjectNodeSchema } from "../objectNodeTypes.js"; -import { isLazy, type LazyItem } from "../flexList.js"; -import { markSchemaMostDerived } from "./schemaFactory.js"; /** * Provides various functions for analyzing {@link TreeNode}s. @@ -212,19 +211,7 @@ export const treeNodeApi: TreeNodeApi = { if (actualSchema === undefined) { return false; } - if (isReadonlyArray>(schema)) { - for (const singleSchema of schema) { - const testSchema = isLazy(singleSchema) ? singleSchema() : singleSchema; - markSchemaMostDerived(testSchema); - if (testSchema === actualSchema) { - return true; - } - } - return false; - } else { - markSchemaMostDerived(schema); - return schema === actualSchema; - } + return normalizeAllowedTypes(schema).has(actualSchema); }, schema(node: TreeNode | TreeLeafValue): TreeNodeSchema { return tryGetSchema(node) ?? fail(0xb37 /* Not a tree node */); diff --git a/packages/dds/tree/src/simple-tree/api/typesUnsafe.ts b/packages/dds/tree/src/simple-tree/api/typesUnsafe.ts index da2f39f46651..e55e01321146 100644 --- a/packages/dds/tree/src/simple-tree/api/typesUnsafe.ts +++ b/packages/dds/tree/src/simple-tree/api/typesUnsafe.ts @@ -8,6 +8,8 @@ import type { RestrictiveStringRecord, UnionToIntersection } from "../../util/in import type { ApplyKind, ApplyKindInput, + CustomizedSchemaTyping, + CustomTypes, FieldKind, FieldSchema, ImplicitAllowedTypes, @@ -25,6 +27,7 @@ import type { } from "../core/index.js"; import type { TreeArrayNode } from "../arrayNode.js"; import type { FlexListToUnion, LazyItem } from "../flexList.js"; +import type { ApplyKindAssignment } from "../objectNode.js"; /* * TODO: @@ -48,17 +51,168 @@ import type { FlexListToUnion, LazyItem } from "../flexList.js"; */ export type Unenforced<_DesiredExtendsConstraint> = unknown; +/** + * {@link Unenforced} version of {@link customizeSchemaTyping} for use with recursive schema types. + * + * @remarks + * When using this API to modify a schema derived type such that the type is no longer recursive, + * or uses an externally defined type (which can be recursive), {@link customizeSchemaTyping} should be used instead for an improved developer experience. + * Additionally, in this case, none of the "unsafe" type variants should be needed: the whole schema (with runtime but not schema derived type recursion) + * should use the normal (not unsafe/recursive) APIs. + * @alpha + */ +export function customizeSchemaTypingUnsafe>( + schema: TSchema, +): CustomizerUnsafe { + // This function just does type branding, and duplicating the typing here to avoid any would just make it harder to maintain not easier: + const f = (): any => schema; + return { simplified: f, simplifiedUnrestricted: f, custom: f }; +} + +/** + * {@link Unenforced} version of `Customizer`. + * @remarks + * This has fewer options than the safe version, but all options can still be expressed using the "custom" method. + * @sealed @public + */ +export interface CustomizerUnsafe> { + /** + * Replace typing with a single substitute type which allowed types must implement. + * @remarks + * This is generally type safe for reading the tree, but allows instances of `T` other than those listed in the schema to be assigned, + * which can be out of schema and err at runtime in the same way {@link CustomizerUnsafe.relaxed} does. + * Until with {@link CustomizerUnsafe.relaxed}, implicit construction is disabled, meaning all nodes must be explicitly constructed (and thus implement `T`) before being inserted. + */ + simplified< + T extends (TreeNode | TreeLeafValue) & TreeNodeFromImplicitAllowedTypesUnsafe, + >(): CustomizedSchemaTyping< + TSchema, + { + input: T; + readWrite: T; + output: T; + } + >; + + /** + * The same as {@link CustomizerUnsafe} except that more T values are allowed, even ones not known to be implemented by `TSchema`. + */ + simplifiedUnrestricted(): CustomizedSchemaTyping< + TSchema, + { + input: T; + readWrite: T; + output: T; + } + >; + + /** + * Fully arbitrary customization. + * Provided types override existing types. + * @remarks + * This can express any of the customizations possible via other {@link CustomizerUnsafe} methods: + * this API is however more verbose and can more easily be used to unsafe typing. + */ + custom>(): CustomizedSchemaTyping< + TSchema, + Pick & { + // Check if property is provided. This check is needed to early out missing values so if undefined is allowed, + // not providing the field doesn't overwrite the corresponding type with undefined. + // TODO: test this case + [Property in keyof CustomTypes]: Property extends keyof T + ? T[Property] extends CustomTypes[Property] + ? T[Property] + : GetTypesUnsafe[Property] + : GetTypesUnsafe[Property]; + } + >; +} + /** * {@link Unenforced} version of `ObjectFromSchemaRecord`. * @remarks - * Do note use this type directly: its only needed in the implementation of generic logic which define recursive schema, not when using recursive schema. + * Do not use this type directly: its only needed in the implementation of generic logic which define recursive schema, not when using recursive schema. * @system @public */ export type ObjectFromSchemaRecordUnsafe< T extends Unenforced>, -> = { - -readonly [Property in keyof T]: TreeFieldFromImplicitFieldUnsafe; -}; +> = + // Due to https://github.com/microsoft/TypeScript/issues/43826 we can not set the desired setter type. + // Attempts to implement this in the cleaner way ObjectFromSchemaRecord uses cause recursive types to fail to compile. + // Supporting explicit field schema wrapping CustomizedSchemaTyping here breaks compilation of recursive cases as well. + { + -readonly [Property in keyof T as [T[Property]] extends [ + CustomizedSchemaTyping< + unknown, + { + readonly readWrite: never; + readonly input: unknown; + readonly output: TreeNode | TreeLeafValue; + } + >, + ] + ? never // Remove readWrite version for cases using CustomizedSchemaTyping to set readWrite to never. + : Property]: AssignableTreeFieldFromImplicitFieldUnsafe; + } & { + readonly [Property in keyof T as [T[Property]] extends [ + CustomizedSchemaTyping< + unknown, + { + readonly readWrite: never; + readonly input: unknown; + readonly output: TreeNode | TreeLeafValue; + } + >, + ] + ? // Inverse of the conditional above: only include readonly fields when not including the readWrite one. This is required to make recursive types compile. + Property + : never]: TreeFieldFromImplicitFieldUnsafe; + }; + +/** + * {@link Unenforced} version of `AssignableTreeFieldFromImplicitField`. + * @remarks + * Do not use this type directly: its only needed in the implementation of generic logic which define recursive schema, not when using recursive schema. + * @privateRemarks + * Recursive version doesn't remove setters when this is never, so this uses covariant not contravariant union handling. + * @system @public + */ +export type AssignableTreeFieldFromImplicitFieldUnsafe< + TSchema extends Unenforced, +> = TSchema extends FieldSchemaUnsafe + ? ApplyKindAssignment["readWrite"], Kind> + : GetTypesUnsafe["readWrite"]; + +/** + * {@link Unenforced} version of `TypesUnsafe`. + * @remarks + * Do not use this type directly: its only needed in the implementation of generic logic which define recursive schema, not when using recursive schema. + * @system @public + */ +export type GetTypesUnsafe> = [ + TSchema, +] extends [CustomizedSchemaTyping] + ? TCustom + : StrictTypesUnsafe; + +/** + * {@link Unenforced} version of `StrictTypes`. + * @remarks + * Do not use this type directly: its only needed in the implementation of generic logic which define recursive schema, not when using recursive schema. + * @system @public + */ +export interface StrictTypesUnsafe< + TSchema extends Unenforced, + TInput = DefaultInsertableTreeNodeFromImplicitAllowedTypesUnsafe, + TOutput = DefaultTreeNodeFromImplicitAllowedTypesUnsafe, +> { + input: TInput; + // Partial mitigation setter limitations (removal of setters when TInput is never by setting this to never) breaks compilation if used here, + // so recursive objects end up allowing some unsafe assignments which will error at runtime. + // This unsafety occurs when schema types are not exact, so output types are generalized which results in setters being generalized (wince they get the same type) which is unsafe. + readWrite: TOutput; // TInput extends never ? never : TOutput; + output: TOutput; +} /** * {@link Unenforced} version of {@link TreeNodeSchema}. @@ -121,7 +275,7 @@ export interface TreeNodeSchemaNonClassUnsafe< /** * {@link Unenforced} version of {@link TreeObjectNode}. * @remarks - * Do note use this type directly: its only needed in the implementation of generic logic which define recursive schema, not when using recursive schema. + * Do not use this type directly: its only needed in the implementation of generic logic which define recursive schema, not when using recursive schema. * @system @public */ export type TreeObjectNodeUnsafe< @@ -132,7 +286,7 @@ export type TreeObjectNodeUnsafe< /** * {@link Unenforced} version of {@link TreeFieldFromImplicitField}. * @remarks - * Do note use this type directly: its only needed in the implementation of generic logic which define recursive schema, not when using recursive schema. + * Do not use this type directly: its only needed in the implementation of generic logic which define recursive schema, not when using recursive schema. * @system @public */ export type TreeFieldFromImplicitFieldUnsafe> = @@ -153,11 +307,21 @@ export type AllowedTypesUnsafe = readonly LazyItem[]; /** * {@link Unenforced} version of {@link TreeNodeFromImplicitAllowedTypes}. * @remarks - * Do note use this type directly: its only needed in the implementation of generic logic which define recursive schema, not when using recursive schema. + * Do not use this type directly: its only needed in the implementation of generic logic which define recursive schema, not when using recursive schema. * @system @public */ export type TreeNodeFromImplicitAllowedTypesUnsafe< TSchema extends Unenforced, +> = GetTypesUnsafe["output"]; + +/** + * {@link Unenforced} version of {@link DefaultTreeNodeFromImplicitAllowedTypesUnsafe}. + * @remarks + * Do not use this type directly: its only needed in the implementation of generic logic which define recursive schema, not when using recursive schema. + * @system @public + */ +export type DefaultTreeNodeFromImplicitAllowedTypesUnsafe< + TSchema extends Unenforced, > = TSchema extends TreeNodeSchemaUnsafe ? NodeFromSchemaUnsafe : TSchema extends AllowedTypesUnsafe @@ -168,11 +332,22 @@ export type TreeNodeFromImplicitAllowedTypesUnsafe< * {@link Unenforced} version of {@link InsertableTreeNodeFromImplicitAllowedTypes}. * @see {@link Input} * @remarks - * Do note use this type directly: its only needed in the implementation of generic logic which define recursive schema, not when using recursive schema. + * Do not use this type directly: its only needed in the implementation of generic logic which define recursive schema, not when using recursive schema. * @system @public */ export type InsertableTreeNodeFromImplicitAllowedTypesUnsafe< TSchema extends Unenforced, +> = GetTypesUnsafe["input"]; + +/** + * {@link Unenforced} version of {@link DefaultInsertableTreeNodeFromImplicitAllowedTypes}. + * @see {@link Input} + * @remarks + * Do not use this type directly: its only needed in the implementation of generic logic which define recursive schema, not when using recursive schema. + * @system @public + */ +export type DefaultInsertableTreeNodeFromImplicitAllowedTypesUnsafe< + TSchema extends Unenforced, > = [TSchema] extends [TreeNodeSchemaUnsafe] ? InsertableTypedNodeUnsafe : [TSchema] extends [AllowedTypesUnsafe] @@ -197,7 +372,7 @@ export type InsertableTreeNodeFromAllowedTypesUnsafe< * {@link Unenforced} version of {@link InsertableTypedNode}. * @see {@link Input} * @remarks - * Do note use this type directly: its only needed in the implementation of generic logic which define recursive schema, not when using recursive schema. + * Do not use this type directly: its only needed in the implementation of generic logic which define recursive schema, not when using recursive schema. * @privateRemarks * TODO: * This is less strict than InsertableTypedNode when given non-exact schema to avoid compilation issues. @@ -216,7 +391,7 @@ export type InsertableTypedNodeUnsafe< /** * {@link Unenforced} version of {@link NodeFromSchema}. * @remarks - * Do note use this type directly: its only needed in the implementation of generic logic which define recursive schema, not when using recursive schema. + * Do not use this type directly: its only needed in the implementation of generic logic which define recursive schema, not when using recursive schema. * @system @public */ export type NodeFromSchemaUnsafe> = @@ -225,7 +400,7 @@ export type NodeFromSchemaUnsafe> = /** * {@link Unenforced} version of {@link InsertableTreeNodeFromImplicitAllowedTypes}. * @remarks - * Do note use this type directly: its only needed in the implementation of generic logic which define recursive schema, not when using recursive schema. + * Do not use this type directly: its only needed in the implementation of generic logic which define recursive schema, not when using recursive schema. * @system @public */ export type NodeBuilderDataUnsafe> = @@ -234,7 +409,7 @@ export type NodeBuilderDataUnsafe> = /** * {@link Unenforced} version of {@link (TreeArrayNode:interface)}. * @remarks - * Do note use this type directly: its only needed in the implementation of generic logic which define recursive schema, not when using recursive schema. + * Do not use this type directly: its only needed in the implementation of generic logic which define recursive schema, not when using recursive schema. * @system @sealed @public */ export interface TreeArrayNodeUnsafe> @@ -247,7 +422,7 @@ export interface TreeArrayNodeUnsafe> @@ -271,7 +446,7 @@ export interface TreeMapNodeUnsafe> * Copy of TypeScript's ReadonlyMap, but with `TreeNodeFromImplicitAllowedTypesUnsafe` inlined into it. * Using this instead of ReadonlyMap in TreeMapNodeUnsafe is necessary to make recursive map schema not generate compile errors in the d.ts files when exported. * @remarks - * Do note use this type directly: its only needed in the implementation of generic logic which define recursive schema, not when using recursive schema. + * Do not use this type directly: its only needed in the implementation of generic logic which define recursive schema, not when using recursive schema. * @privateRemarks * This is the same as `ReadonlyMap>` (Checked in test), * except that it avoids the above mentioned compile error. @@ -313,7 +488,7 @@ export interface ReadonlyMapInlined> = @@ -328,7 +503,7 @@ export type FieldHasDefaultUnsafe> = * {@link Unenforced} version of `InsertableObjectFromSchemaRecord`. * @see {@link Input} * @remarks - * Do note use this type directly: its only needed in the implementation of generic logic which define recursive schema, not when using recursive schema. + * Do not use this type directly: its only needed in the implementation of generic logic which define recursive schema, not when using recursive schema. * @system @public */ export type InsertableObjectFromSchemaRecordUnsafe< @@ -350,7 +525,7 @@ export type InsertableObjectFromSchemaRecordUnsafe< * {@link Unenforced} version of {@link InsertableTreeFieldFromImplicitField}. * @see {@link Input} * @remarks - * Do note use this type directly: its only needed in the implementation of generic logic which define recursive schema, not when using recursive schema. + * Do not use this type directly: its only needed in the implementation of generic logic which define recursive schema, not when using recursive schema. * @system @public */ export type InsertableTreeFieldFromImplicitFieldUnsafe< @@ -365,7 +540,7 @@ export type InsertableTreeFieldFromImplicitFieldUnsafe< /** * {@link Unenforced} version of {@link FieldSchema}. * @remarks - * Do note use this type directly: its only needed in the implementation of generic logic which define recursive schema, not when using recursive schema. + * Do not use this type directly: its only needed in the implementation of generic logic which define recursive schema, not when using recursive schema. * @public */ export interface FieldSchemaUnsafe< diff --git a/packages/dds/tree/src/simple-tree/flexList.ts b/packages/dds/tree/src/simple-tree/flexList.ts index 830596a374f6..bce6fcd34bd6 100644 --- a/packages/dds/tree/src/simple-tree/flexList.ts +++ b/packages/dds/tree/src/simple-tree/flexList.ts @@ -44,28 +44,18 @@ export function markEager(t: T): T { * By default, items that are of type `"function"` will be considered lazy and all other items will be considered eager. * To force a `"function"` item to be treated as an eager item, call `markEager` before putting it in the list. * This is necessary e.g. when the eager list items are function types and the lazy items are functions that _return_ function types. - * `FlexList`s are processed by `normalizeFlexList` and `normalizeFlexListEager`. + * Our one use of FlexList has some special normalization logic, see {@link normalizeAllowedTypes}. * @system @public */ export type FlexList = readonly LazyItem[]; -/** - * Given a `FlexList` of eager and lazy items, return an equivalent list where all items are eager. - */ -export function normalizeFlexListEager(t: FlexList): T[] { - const data: T[] = t.map((value: LazyItem) => { - if (isLazy(value)) { - return value(); - } - return value; - }); - return data; -} - /** * An "eager" or "lazy" Item in a `FlexList`. * Lazy items are wrapped in a function to allow referring to themselves before they are declared. * This makes recursive and co-recursive items possible. + * @privateRemarks + * `schemaTypes.ts`'s `evaluateLazySchema` (via {@link normalizeAllowedTypes}) + * applies caching for the only current use of this type. * @public */ export type LazyItem = Item | (() => Item); diff --git a/packages/dds/tree/src/simple-tree/index.ts b/packages/dds/tree/src/simple-tree/index.ts index 4e8376c1c050..815de6ab675b 100644 --- a/packages/dds/tree/src/simple-tree/index.ts +++ b/packages/dds/tree/src/simple-tree/index.ts @@ -124,6 +124,11 @@ export { type CustomTreeNode, type CustomTreeValue, tryStoredSchemaAsArray, + type GetTypesUnsafe, + type DefaultInsertableTreeNodeFromImplicitAllowedTypesUnsafe, + type DefaultTreeNodeFromImplicitAllowedTypesUnsafe, + type StrictTypesUnsafe, + type AssignableTreeFieldFromImplicitFieldUnsafe, type schemaStatics, type ITreeAlpha, type TransactionConstraint, @@ -136,6 +141,7 @@ export { type TransactionResultSuccess, type TransactionResultFailed, rollback, + Component, } from "./api/index.js"; export { type NodeFromSchema, @@ -170,6 +176,17 @@ export { type ReadSchema, type NodeSchemaOptions, type NodeSchemaMetadata, + customizeSchemaTyping, + type DefaultTreeNodeFromImplicitAllowedTypes, + type Customizer, + type GetTypes, + type StrictTypes, + type CustomTypes, + type CustomizedSchemaTyping, + CustomizedTyping, + type DefaultInsertableTreeNodeFromImplicitAllowedTypes, + type SchemaUnionToIntersection, + evaluateLazySchema, } from "./schemaTypes.js"; export { getTreeNodeForField, @@ -184,6 +201,8 @@ export { type FieldHasDefault, type InsertableObjectFromSchemaRecord, type ObjectFromSchemaRecord, + type AssignableTreeFieldFromImplicitField, + type ApplyKindAssignment, type TreeObjectNode, setField, createUnknownOptionalFieldPolicy, @@ -203,4 +222,9 @@ export { handleSchema, nullSchema, } from "./leafNodeSchema.js"; -export type { LazyItem, FlexList, FlexListToUnion, ExtractItemType } from "./flexList.js"; +export type { + LazyItem, + FlexList, + FlexListToUnion, + ExtractItemType, +} from "./flexList.js"; diff --git a/packages/dds/tree/src/simple-tree/objectNode.ts b/packages/dds/tree/src/simple-tree/objectNode.ts index efccc6a70107..746c1d2f5a99 100644 --- a/packages/dds/tree/src/simple-tree/objectNode.ts +++ b/packages/dds/tree/src/simple-tree/objectNode.ts @@ -27,6 +27,8 @@ import { type ImplicitAllowedTypes, FieldKind, type NodeSchemaMetadata, + type GetTypes, + type SchemaUnionToIntersection, } from "./schemaTypes.js"; import { type TreeNodeSchema, @@ -56,11 +58,51 @@ import { getUnhydratedContext } from "./createContext.js"; * @system @public */ export type ObjectFromSchemaRecord> = { - -readonly [Property in keyof T]: Property extends string - ? TreeFieldFromImplicitField - : unknown; + // Due to https://github.com/microsoft/TypeScript/issues/43826 we can not set the desired setter type, + // but we can at least remove the setter (by setting the key to never) when there should be no setter. + -readonly [Property in keyof T as [ + AssignableTreeFieldFromImplicitField, + // If the types we want to allow setting to are just never or undefined, remove the setter + ] extends [never | undefined] + ? never + : Property]: AssignableTreeFieldFromImplicitField; +} & { + readonly [Property in keyof T]: TreeFieldFromImplicitField; }; +/** + * Type of content that can be assigned to a field of the given schema. + * + * @see {@link Input} + * + * @typeparam TSchemaInput - Schema to process. + * @typeparam TSchema - Do not specify: default value used as an implementation detail. + * @system @public + */ +export type AssignableTreeFieldFromImplicitField< + TSchemaInput extends ImplicitFieldSchema, + TSchema = SchemaUnionToIntersection, +> = [TSchema] extends [FieldSchema] + ? ApplyKindAssignment["readWrite"], Kind> + : [TSchema] extends [ImplicitAllowedTypes] + ? GetTypes["readWrite"] + : never; + +/** + * Suitable for assignment. + * + * @see {@link Input} + * @system @public + */ +export type ApplyKindAssignment = [Kind] extends [ + FieldKind.Required, +] + ? T + : [Kind] extends [FieldKind.Optional] + ? T | undefined + : // Unknown, non-exact and identifier fields are not assignable. + never; + /** * A {@link TreeNode} which modules a JavaScript object. * @remarks @@ -340,6 +382,9 @@ export function objectSchema< metadata?: NodeSchemaMetadata, ): ObjectNodeSchema & ObjectNodeSchemaInternalData { + // Field set can't be modified after since derived data is stored in maps. + Object.freeze(info); + // Ensure no collisions between final set of property keys, and final set of stored keys (including those // implicitly derived from property keys) assertUniqueKeys(identifier, info); diff --git a/packages/dds/tree/src/simple-tree/schemaTypes.ts b/packages/dds/tree/src/simple-tree/schemaTypes.ts index f2e9e44acb03..fdc719715d47 100644 --- a/packages/dds/tree/src/simple-tree/schemaTypes.ts +++ b/packages/dds/tree/src/simple-tree/schemaTypes.ts @@ -16,19 +16,23 @@ import { compareSets, type requireTrue, type areOnlyKeys, + getOrCreate, } from "../util/index.js"; -import type { - Unhydrated, - NodeKind, - TreeNodeSchema, - TreeNodeSchemaClass, - TreeNode, - TreeNodeSchemaCore, - TreeNodeSchemaNonClass, +import { + type Unhydrated, + type NodeKind, + type TreeNodeSchema, + type TreeNodeSchemaClass, + type TreeNode, + type TreeNodeSchemaCore, + type TreeNodeSchemaNonClass, + inPrototypeChain, } from "./core/index.js"; import type { FieldKey } from "../core/index.js"; import type { InsertableContent } from "./toMapTree.js"; import { isLazy, type FlexListToUnion, type LazyItem } from "./flexList.js"; +import { LeafNodeSchema } from "./leafNodeSchema.js"; +import { TreeNodeValid } from "./treeNodeValid.js"; /** * Returns true if the given schema is a {@link TreeNodeSchemaClass}, or otherwise false if it is a {@link TreeNodeSchemaNonClass}. @@ -69,6 +73,8 @@ export function isTreeNodeSchemaClass< * way to declare and manipulate unordered sets of types in TypeScript. * * Not intended for direct use outside of package. + * @privateRemarks + * Code reading data from this should use `normalizeAllowedTypes` to ensure consistent handling, caching, nice errors etc. * @system @public */ export type AllowedTypes = readonly LazyItem[]; @@ -379,6 +385,8 @@ export function normalizeAllowedTypes( ): ReadonlySet { const normalized = new Set(); if (isReadonlyArray(types)) { + // Types array must not be modified after it is normalized since that would result if the user of the normalized data having wrong (out of date) content. + Object.freeze(types); for (const lazyType of types) { normalized.add(evaluateLazySchema(lazyType)); } @@ -469,16 +477,59 @@ function areMetadataEqual( return a?.custom === b?.custom && a?.description === b?.description; } -function evaluateLazySchema(value: LazyItem): TreeNodeSchema { - const evaluatedSchema = isLazy(value) ? value() : value; +const cachedLazyItem = new WeakMap<() => unknown, unknown>(); + +/** + * Returns the schema referenced by the {@link LazyItem}. + * @remarks + * Caches results to handle {@link LazyItem} which compute their resulting schema. + * @alpha + */ +export function evaluateLazySchema(value: LazyItem): T { + const evaluatedSchema = isLazy(value) + ? (getOrCreate(cachedLazyItem, value, value) as T) + : value; if (evaluatedSchema === undefined) { throw new UsageError( `Encountered an undefined schema. This could indicate that some referenced schema has not yet been instantiated.`, ); } + markSchemaMostDerived(evaluatedSchema); return evaluatedSchema; } +/** + * Indicates that a schema is the "most derived" version which is allowed to be used, see {@link MostDerivedData}. + * Calling helps with error messages about invalid schema usage (using more than one type from single schema factor produced type, + * and thus calling this for one than one subclass). + * @remarks + * Helper for invoking {@link TreeNodeValid.markMostDerived} for any {@link TreeNodeSchema} if it needed. + */ +export function markSchemaMostDerived( + schema: TreeNodeSchema, + oneTimeInitialize = false, +): void { + if (schema instanceof LeafNodeSchema) { + return; + } + + if (!inPrototypeChain(schema, TreeNodeValid)) { + // Use JSON.stringify to quote and escape identifier string. + throw new UsageError( + `Schema for ${JSON.stringify( + schema.identifier, + )} does not extend a SchemaFactory generated class. This is invalid.`, + ); + } + + const schemaValid = schema as typeof TreeNodeValid & TreeNodeSchema; + if (oneTimeInitialize) { + schemaValid.oneTimeInitialize(); + } else { + schemaValid.markMostDerived(); + } +} + /** * Types of {@link TreeNode|TreeNodes} or {@link TreeLeafValue|TreeLeafValues} allowed at a location in a tree. * @remarks @@ -508,6 +559,8 @@ function evaluateLazySchema(value: LazyItem): TreeNodeSchema { * class A extends sf.array("example", [() => B]) {} * class B extends sf.array("Inner", sf.number) {} * ``` + * @privateRemarks + * Code reading data from this should use `normalizeAllowedTypes` to ensure consistent handling, caching, nice errors etc. * @public */ export type ImplicitAllowedTypes = AllowedTypes | TreeNodeSchema; @@ -546,13 +599,27 @@ export type TreeFieldFromImplicitField, + TSchema = [TSchemaInput] extends [CustomizedSchemaTyping] + ? TSchemaInput + : SchemaUnionToIntersection, > = [TSchema] extends [FieldSchema] ? ApplyKindInput, Kind, true> : [TSchema] extends [ImplicitAllowedTypes] ? InsertableTreeNodeFromImplicitAllowedTypes : never; +/** + * {@link UnionToIntersection} except it does not distribute over {@link CustomizedSchemaTyping}s when the original type is a union. + * @privateRemarks + * This is a workaround for TypeScript distributing over intersections over unions when distributing extends over unions. + * @system @public + */ +export type SchemaUnionToIntersection = [T] extends [ + CustomizedSchemaTyping, +] + ? T + : UnionToIntersection; + /** * {@inheritdoc (UnsafeUnknownSchema:type)} * @alpha @@ -575,22 +642,228 @@ export const UnsafeUnknownSchema: unique symbol = Symbol("UnsafeUnknownSchema"); * Any APIs which use this must produce UsageErrors when out of schema data is encountered, and never produce unrecoverable errors, * or silently accept invalid data. * This is currently only type exported from the package: the symbol is just used as a way to get a named type. + * + * TODO: This takes a very different approach than `customizeSchemaTyping` which applies to allowed types. + * Maybe generalize that to apply to field schema as well and replace this with it? * @alpha */ export type UnsafeUnknownSchema = typeof UnsafeUnknownSchema; +/** + * {@inheritdoc (CustomizedTyping:type)} + * @system @public + */ +export const CustomizedTyping: unique symbol = Symbol("CustomizedTyping"); + +/** + * A type brand used by {@link customizeSchemaTyping}. + * @system @public + */ +export type CustomizedTyping = typeof CustomizedTyping; + +/** + * Collection of schema aware types. + * @remarks + * This type is only used as a type constraint. + * It's fields are similar to an unordered set of generic type parameters. + * {@link customizeSchemaTyping} applies this to {@link ImplicitAllowedTypes} via {@link CustomizedSchemaTyping}. + * @sealed @public + */ +export interface CustomTypes { + /** + * Type used for inserting values. + */ + readonly input: unknown; + /** + * Type used for the read+write property on object nodes. + * + * Set to never to disable setter. + * @remarks + * Due to https://github.com/microsoft/TypeScript/issues/43826 we cannot set the desired setter type. + * Instead we can only control the types of the read+write property and the type of a readonly property. + * + * For recursive types using {@link SchemaFactory.objectRecursive}, support for using `never` to remove setters is limited: + * When the customized schema is wrapped in an {@link FieldSchema}, the setter will not be fully removed. + */ + readonly readWrite: TreeLeafValue | TreeNode; + /** + * Type for reading data. + * @remarks + * See limitation for read+write properties on ObjectNodes in {@link CustomTypes.readWrite}. + */ + readonly output: TreeLeafValue | TreeNode; +} + +/** + * Type annotation which overrides the default schema derived types with customized ones. + * @remarks + * See {@link customizeSchemaTyping} for more information. + * @system @public + */ +export type CustomizedSchemaTyping = TSchema & { + [CustomizedTyping]: TCustom; +}; + +/** + * Default strict policy. + * + * @typeparam TSchema - The schema to process + * @typeparam TInput - Internal: do not specify. + * @typeparam TOutput - Internal: do not specify. + * @remarks + * Handles input types contravariantly so any input which might be invalid is rejected. + * @sealed @public + */ +export interface StrictTypes< + TSchema extends ImplicitAllowedTypes, + TInput = DefaultInsertableTreeNodeFromImplicitAllowedTypes, + TOutput extends TreeNode | TreeLeafValue = DefaultTreeNodeFromImplicitAllowedTypes, +> { + input: TInput; + readWrite: TInput extends never ? never : TOutput; + output: TOutput; +} + +/** + * Customizes the types associated with `TSchema` + * @remarks + * By default, the types used when constructing, reading and writing tree nodes are derived from the schema. + * In some cases, it may be desirable to override these types with carefully selected alternatives. + * This utility allows for that customization. + * Note that this customization is only used for typing, and does not affect the runtime behavior at all. + * + * This can be used for a wide variety of purposes, including (but not limited to): + * + * 1. Implementing better typing for a runtime extensible set of types (e.g. a polymorphic collection). + * This is commonly needed when implementing containers which don't directly reference their child types, and can be done using {@link Customizer.simplified}. + * 2. Adding type brands to specific values to increase type safety. + * This can be done using {@link Customizer.simplified}. + * 3. Adding some (compile time only) constraints to values, like enum style unions. + * This can be done using {@link Customizer.simplified}. + * 4. Making fields readonly (for the current client). + * This can be done using {@link Customizer.custom} with `{ readWrite: never; }`. + * 5. Opting into more [compleat and less sound](https://en.wikipedia.org/wiki/Soundness#Relation_to_completeness) typing. + * {@link Customizer.relaxed} is an example of this. + * + * For this customization to be used, the resulting schema must be used as `ImplicitAllowedTypes`. + * For example applying this to a single type, then using that type in an array of allowed types will have no effect: + * in such a case the customization must instead be applied to the array of allowed types. + * @privateRemarks + * Once this API is more stable/final, the examples in tests such as openPolymorphism.spec.ts and schemaFactory.examples.spec.ts + * should be copied into examples here, or somehow linked. + * @alpha + */ +export function customizeSchemaTyping( + schema: TSchema, +): Customizer { + // This function just does type branding, and duplicating the typing here to avoid any would just make it harder to maintain not easier: + // eslint-disable-next-line @typescript-eslint/no-explicit-any + const f = (): any => schema; + return { strict: f, relaxed: f, simplified: f, simplifiedUnrestricted: f, custom: f }; +} + +/** + * Utility for customizing the types used for data matching a given schema. + * @sealed @alpha + */ +export interface Customizer { + /** + * The default {@link StrictTypes}, explicitly applied. + */ + strict(): CustomizedSchemaTyping>; + /** + * Relaxed policy: allows possible invalid edits (which will err at runtime) when schema is not exact. + * @remarks + * Handles input types covariantly so any input which might be valid with the schema is allowed + * instead of the default strict policy of only inputs with all possible schema are allowed. + * + * This only modifies the typing shallowly: the typing of children are not effected. + */ + relaxed(): CustomizedSchemaTyping< + TSchema, + { + input: TreeNodeSchema extends TSchema + ? InsertableContent + : // This intentionally distributes unions over the conditional to get covariant type handling. + TSchema extends TreeNodeSchema + ? InsertableTypedNode + : // This intentionally distributes unions over the conditional to get covariant type handling. + TSchema extends AllowedTypes + ? TSchema[number] extends LazyItem + ? InsertableTypedNode + : never + : never; + readWrite: TreeNodeFromImplicitAllowedTypes; + output: TreeNodeFromImplicitAllowedTypes; + } + >; + /** + * Replace typing with a single substitute which allowed types must implement. + * @remarks + * This is generally type safe for reading the tree, but allows instances of `T` other than those listed in the schema to be assigned, + * which can be out of schema and err at runtime in the same way {@link Customizer.relaxed} does. + * Until with {@link Customizer.relaxed}, implicit construction is disabled, meaning all nodes must be explicitly constructed (and thus implement `T`) before being inserted. + */ + simplified>(): CustomizedSchemaTyping< + TSchema, + { + input: T; + readWrite: T; + output: T; + } + >; + + /** + * The same as {@link Customizer} except that more T values are allowed, even ones not known to be implemented by `TSchema`. + */ + simplifiedUnrestricted(): CustomizedSchemaTyping< + TSchema, + { + input: T; + readWrite: T; + output: T; + } + >; + + /** + * Fully arbitrary customization. + * Provided types override existing types. + */ + custom>(): CustomizedSchemaTyping< + TSchema, + { + // Check if property is provided. This check is needed to early out missing values so if undefined is allowed, + // not providing the field doesn't overwrite the corresponding type with undefined. + // TODO: test this case + [Property in keyof CustomTypes]: Property extends keyof T + ? T[Property] extends CustomTypes[Property] + ? T[Property] + : GetTypes[Property] + : GetTypes[Property]; + } + >; +} + +/** + * Fetch types associated with a schema, or use the default if not customized. + * @system @public + */ +export type GetTypes = [TSchema] extends [ + CustomizedSchemaTyping, +] + ? TCustom + : StrictTypes; + /** * Content which could be inserted into a tree. * * @see {@link Input} * @remarks - * Extended version of {@link InsertableTreeNodeFromImplicitAllowedTypes} that also allows {@link (UnsafeUnknownSchema:type)}. + * Alias of {@link InsertableTreeNodeFromImplicitAllowedTypes} with a shorter name. * @alpha */ -export type Insertable = - TSchema extends ImplicitAllowedTypes - ? InsertableTreeNodeFromImplicitAllowedTypes - : InsertableContent; +export type Insertable = + InsertableTreeNodeFromImplicitAllowedTypes; /** * Content which could be inserted into a field within a tree. @@ -664,10 +937,22 @@ export type ApplyKindInput = GetTypes["output"]; + +/** + * Default type of tree node for a field of the given schema. + * @system @public + */ +export type DefaultTreeNodeFromImplicitAllowedTypes< + TSchema extends ImplicitAllowedTypes = TreeNodeSchema, > = TSchema extends TreeNodeSchema ? NodeFromSchema : TSchema extends AllowedTypes @@ -727,6 +1012,17 @@ export type TreeNodeFromImplicitAllowedTypes< */ export type Input = T; +/** + * Type of content that can be inserted into the tree for a node of the given schema. + * + * @typeparam TSchema - Schema to process. + * @remarks + * Defaults to {@link DefaultInsertableTreeNodeFromImplicitAllowedTypes}. + * @public + */ +export type InsertableTreeNodeFromImplicitAllowedTypes = + GetTypes["input"]; + /** * Type of content that can be inserted into the tree for a node of the given schema. * @@ -736,14 +1032,15 @@ export type Input = T; * * @privateRemarks * This is a bit overly conservative, since cases like `A | [A]` give never and could give `A`. - * @public + * @system @public */ -export type InsertableTreeNodeFromImplicitAllowedTypes = - [TSchema] extends [TreeNodeSchema] - ? InsertableTypedNode - : [TSchema] extends [AllowedTypes] - ? InsertableTreeNodeFromAllowedTypes - : never; +export type DefaultInsertableTreeNodeFromImplicitAllowedTypes< + TSchema extends ImplicitAllowedTypes, +> = [TSchema] extends [TreeNodeSchema] + ? InsertableTypedNode + : [TSchema] extends [AllowedTypes] + ? InsertableTreeNodeFromAllowedTypes + : never; /** * Type of content that can be inserted into the tree for a node of the given schema. @@ -793,6 +1090,8 @@ export type NodeFromSchema = T extends TreeNodeSchemaC * One special case this makes is if the result of NodeFromSchema contains TreeNode, this must be an under constrained schema, so the result is set to never. * Note that applying UnionToIntersection on the result of NodeFromSchema does not work since it breaks booleans. * + * Some internal code may use second parameter to opt out of contravariant behavior, but this is not a stable API. + * * @public */ export type InsertableTypedNode< diff --git a/packages/dds/tree/src/simple-tree/toStoredSchema.ts b/packages/dds/tree/src/simple-tree/toStoredSchema.ts index c06c89b99ba4..36694cfdbdf0 100644 --- a/packages/dds/tree/src/simple-tree/toStoredSchema.ts +++ b/packages/dds/tree/src/simple-tree/toStoredSchema.ts @@ -20,18 +20,18 @@ import { type TreeTypeSet, } from "../core/index.js"; import { FieldKinds, type FlexFieldKind } from "../feature-libraries/index.js"; -import { brand, fail, getOrCreate, isReadonlyArray } from "../util/index.js"; +import { brand, fail, getOrCreate } from "../util/index.js"; import { NodeKind, type TreeNodeSchema } from "./core/index.js"; import { FieldKind, FieldSchema, + normalizeAllowedTypes, type ImplicitAllowedTypes, type ImplicitFieldSchema, } from "./schemaTypes.js"; import { walkFieldSchema } from "./walkFieldSchema.js"; import { LeafNodeSchema } from "./leafNodeSchema.js"; import { isObjectNodeSchema } from "./objectNodeTypes.js"; -import { normalizeFlexListEager } from "./flexList.js"; const viewToStoredCache = new WeakMap(); @@ -92,10 +92,7 @@ const convertFieldKind = new Map([ * Normalizes an {@link ImplicitAllowedTypes} into an {@link TreeTypeSet}. */ export function convertAllowedTypes(schema: ImplicitAllowedTypes): TreeTypeSet { - if (isReadonlyArray(schema)) { - return new Set(normalizeFlexListEager(schema).map((item) => brand(item.identifier))); - } - return new Set([brand(schema.identifier)]); + return new Set([...normalizeAllowedTypes(schema)].map((item) => brand(item.identifier))); } /** diff --git a/packages/dds/tree/src/simple-tree/treeNodeValid.ts b/packages/dds/tree/src/simple-tree/treeNodeValid.ts index 7627119dfb53..5771a81e94ef 100644 --- a/packages/dds/tree/src/simple-tree/treeNodeValid.ts +++ b/packages/dds/tree/src/simple-tree/treeNodeValid.ts @@ -64,6 +64,8 @@ export abstract class TreeNodeValid extends TreeNode { /** * Schema classes can override to provide a callback that is called once when the first node is constructed. * This is a good place to perform extra validation and cache schema derived data needed for the implementation of the node. + * @remarks + * It is valid to dereference LazyItem schema references in this function (or anything that runs after it). */ protected static oneTimeSetup(this: typeof TreeNodeValid): Context { fail(0xae5 /* Missing oneTimeSetup */); @@ -146,7 +148,7 @@ export abstract class TreeNodeValid extends TreeNode { } /** - * @see {@link TreeNodeSchemaCore.createFromInsertable}. + * See {@link TreeNodeSchemaCore.createFromInsertable}. */ public static createFromInsertable TOut>( this: TThis, @@ -155,13 +157,22 @@ export abstract class TreeNodeValid extends TreeNode { return new this(input); } + /** + * Idempotent initialization function that pre-caches data and can dereference lazy schema references. + */ + public static oneTimeInitialize( + this: typeof TreeNodeValid & TreeNodeSchema, + ): Required { + const cache = this.markMostDerived(); + cache.oneTimeInitialized ??= this.oneTimeSetup(); + // Typescript fails to narrow the type of `oneTimeInitialized` to `Context` here, so use a cast: + return cache as MostDerivedData & { oneTimeInitialized: Context }; + } + public constructor(input: TInput | InternalTreeNode) { super(privateToken); const schema = this.constructor as typeof TreeNodeValid & TreeNodeSchema; - const cache = schema.markMostDerived(); - if (cache.oneTimeInitialized === undefined) { - cache.oneTimeInitialized = schema.oneTimeSetup(); - } + const cache = schema.oneTimeInitialize(); if (isTreeNode(input)) { // TODO: update this once we have better support for deep-copying and move operations. diff --git a/packages/dds/tree/src/test/openPolymorphism.spec.ts b/packages/dds/tree/src/test/openPolymorphism.spec.ts new file mode 100644 index 000000000000..f0ab48d87938 --- /dev/null +++ b/packages/dds/tree/src/test/openPolymorphism.spec.ts @@ -0,0 +1,388 @@ +/*! + * Copyright (c) Microsoft Corporation and contributors. All rights reserved. + * Licensed under the MIT License. + */ + +import { strict as assert } from "node:assert"; + +import { + Component, + SchemaFactory, + TreeViewConfiguration, + type NodeKind, + type ObjectFromSchemaRecord, + type TreeNode, + type TreeNodeSchema, + type Unhydrated, +} from "../simple-tree/index.js"; +import { Tree } from "../shared-tree/index.js"; +import { validateUsageError } from "./utils.js"; +import { customizeSchemaTyping, evaluateLazySchema } from "../simple-tree/index.js"; + +const sf = new SchemaFactory("test"); + +/** + * Schema used in example. + */ +class Point extends sf.object("Point", { x: sf.number, y: sf.number }) {} + +// #region Example definition of a polymorphic Component named "Item" +// This code defines what an Item is and how to implement it, but does not depend on any of the implementations. +// Instead implementations depend on this, inverting the normal dependency direction for schema. + +/** + * Fields all Items must have. + */ +const itemFields = { location: Point }; + +/** + * Properties all item types must implement. + */ +interface ItemExtensions { + foo(): void; +} + +/** + * An Item node. + * @remarks + * Open polymorphic collection which libraries can provide additional implementations of, similar to TypeScript interfaces. + * Implementations should declare schema who's nodes extends this interface, and have the schema statically implement ItemSchema. + */ +type Item = TreeNode & ItemExtensions & ObjectFromSchemaRecord; + +/** + * Details about the type all item schema must provide. + * @remarks + * This pattern can be used for for things like generating insert content menus which can describe and create any of the allowed child types. + */ +interface ItemStatic { + readonly description: string; + default(): Unhydrated; +} + +/** + * A schema for an Item. + */ +type ItemSchema = TreeNodeSchema & ItemStatic; + +// #endregion + +/** + * Example implementation of an Item. + */ +class TextItem + extends sf.object("TextItem", { ...itemFields, text: sf.string }) + implements Item +{ + public static readonly description = "Text"; + public static default(): TextItem { + return new TextItem({ text: "", location: { x: 0, y: 0 } }); + } + + public foo(): void { + this.text += "foo"; + } +} + +describe("Open Polymorphism design pattern examples and tests for them", () => { + describe("mutable static registry", () => { + it("without customizeSchemaTyping", () => { + // ------------- + // Registry for items. If using this pattern, this would typically be defined alongside the Item interface. + + /** + * Item type registry. + * @remarks + * This doesn't have to be a mutable static. + * For example libraries could export their implementations instead of adding them when imported, + * then the top level code which pulls in all the libraries could aggregate the item types. + * + * TODO: document (and enforce/detect) when how late it is safe to modify array's used as allowed types. + * These docs should ideally align with how late lazy type lambdas are evaluated (when the tree configuration is constructed, or an instance is made, which ever is first? Maybe define schema finalization?) + */ + const ItemTypes: ItemSchema[] = []; + + // ------------- + // Library using an Item + + class Container extends sf.array("Container", ItemTypes) {} + + // ------------- + // Library defining an item + + ItemTypes.push(TextItem); + + // ------------- + // Example use of container with generic code and down casting + + const container = new Container(); + + // If we don't do anything special, the insertable type is never, so a cast is required to insert content. + // See example using customizeSchemaTyping for how to avoid this. + container.insertAtStart(new TextItem({ text: "", location: { x: 0, y: 0 } }) as never); + + // Items read from the container are typed as Item and have thew expected APIs: + const first = container[0]; + first.foo(); + first.location.x += 1; + + // Down casting works as normal. + if (Tree.is(first, TextItem)) { + assert.equal(first.text, "foo"); + } + }); + + it("error cases", () => { + const ItemTypes: ItemSchema[] = []; + class Container extends sf.array("Container", ItemTypes) {} + + // Not added to registry + // ItemTypes.push(TextItem); + + const container = new Container(); + + // Should error due to out of schema content + assert.throws( + () => + container.insertAtStart( + new TextItem({ text: "", location: { x: 0, y: 0 } }) as never, + ), + validateUsageError(/schema/), + ); + + // Modifying registration too late should error + assert.throws(() => ItemTypes.push(TextItem)); + }); + + it("recursive case", () => { + const ItemTypes: ItemSchema[] = []; + + // Example recursive item implementation + class Container extends sf.array("Container", ItemTypes) {} + class ContainerItem extends sf.object("ContainerItem", { + ...itemFields, + container: Container, + }) { + public static readonly description = "Text"; + public static default(): TextItem { + return new TextItem({ text: "", location: { x: 0, y: 0 } }); + } + + public foo(): void {} + } + + ItemTypes.push(ContainerItem); + + const container = new Container(); + + container.insertAtStart( + new ContainerItem({ container: [], location: { x: 0, y: 0 } }) as never, + ); + }); + + it("safer editing API with customizeSchemaTyping", () => { + const ItemTypes: ItemSchema[] = []; + class Container extends sf.object("Container", { + // Here we force the insertable type to be `Item`, allowing for a potentially unsafe (runtime checked against the schema registrations) insertion of any Item type. + // This avoids the issue from the first example where the insertable type is `never`. + child: sf.optional(customizeSchemaTyping(ItemTypes).simplified()), + }) {} + + ItemTypes.push(TextItem); + + const container = new Container({ child: undefined }); + const container2 = new Container({ child: TextItem.default() }); + + // Enabled by customizeSchemaTyping + container.child = TextItem.default(); + container.child = undefined; + + // Allowed at compile time, but not allowed by schema: + class DisallowedItem + extends sf.object("DisallowedItem", { ...itemFields }) + implements Item + { + public foo(): void {} + } + + // Invalid TreeNodes are rejected at runtime even if allowed at compile time: + assert.throws( + () => { + container.child = new DisallowedItem({ location: { x: 0, y: 0 } }); + }, + validateUsageError(/Invalid schema/), + ); + + // Invalid insertable content is rejected. + // Different use of customizeSchemaTyping could have allowed this at compile time by not including TreeNode in Item. + assert.throws( + () => { + // eslint-disable-next-line @typescript-eslint/consistent-type-assertions + container.child = {} as Item; + }, + validateUsageError(/incompatible with all of the types allowed by the schema/), + ); + }); + + // Example component design pattern which avoids the mutable static registry and instead composes declarative components. + it("components", () => { + /** + * Example application component interface. + */ + interface MyAppComponent { + itemTypes(lazyConfig: () => MyAppConfig): LazyItems; + } + + type LazyItems = readonly (() => ItemSchema)[]; + + function composeComponents(allComponents: readonly MyAppComponent[]): MyAppConfig { + const lazyConfig = () => config; + const ItemTypes = allComponents.flatMap( + (component): LazyItems => component.itemTypes(lazyConfig), + ); + + const config: MyAppConfig = { ItemTypes }; + + return config; + } + + interface MyAppConfig { + readonly ItemTypes: LazyItems; + } + + function createContainer(config: MyAppConfig): ItemSchema { + class Container extends sf.array("Container", config.ItemTypes) {} + class ContainerItem extends sf.object("ContainerItem", { + ...itemFields, + container: Container, + }) { + public static readonly description = "Text"; + public static default(): TextItem { + return new TextItem({ text: "", location: { x: 0, y: 0 } }); + } + + public foo(): void {} + } + + return ContainerItem; + } + + const containerComponent: MyAppComponent = { + itemTypes(lazyConfig: () => MyAppConfig): LazyItems { + return [() => createContainer(lazyConfig())]; + }, + }; + + const textComponent: MyAppComponent = { + itemTypes(): LazyItems { + return [() => TextItem]; + }, + }; + + const appConfig = composeComponents([containerComponent, textComponent]); + + const treeConfig = new TreeViewConfiguration({ + schema: appConfig.ItemTypes, + enableSchemaValidation: true, + preventAmbiguity: true, + }); + }); + + it("generic components system", () => { + // App specific // + + /** + * Subset of `MyAppConfig` which is available while composing components. + */ + interface MyAppConfigPartial { + /** + * {@link AllowedTypes} containing all ItemSchema contributed by components. + */ + readonly allowedItemTypes: Component.LazyArray; + } + + /** + * Example configuration type for an application. + * + * Contains a collection of schema to demonstrate how ComponentSchemaCollection works for schema dependency inversions. + */ + interface MyAppConfig extends MyAppConfigPartial { + /** + * Set of all ItemSchema contributed by components. + * @remarks + * Same content as {@link MyAppConfig.allowedItemTypes}, but normalized into a Set. + */ + readonly items: ReadonlySet; + } + + /** + * Example component type for an application. + * + * Represents functionality provided by a code library to power a component withing the application. + * + * This example uses ComponentSchemaCollection to allow the component to define schema which reference collections of schema from the application configuration. + * This makes it possible to implement the "open polymorphism" pattern, including handling recursive cases. + */ + interface MyAppComponent { + readonly itemTypes: Component.ComponentSchemaCollection< + MyAppConfigPartial, + ItemSchema + >; + } + + /** + * The application specific compose logic. + * + * Information from the components can be aggregated into the configuration. + */ + function composeComponents(allComponents: readonly MyAppComponent[]): MyAppConfig { + const lazyConfig = () => config; + const ItemTypes = Component.composeComponentSchema( + allComponents.map((c) => c.itemTypes), + lazyConfig, + ); + const config: MyAppConfigPartial = { + allowedItemTypes: ItemTypes, + }; + const items = new Set(ItemTypes.map(evaluateLazySchema)); + return { ...config, items }; + } + + // An example simple component + const textComponent: MyAppComponent = { + itemTypes: (): Component.LazyArray => [() => TextItem], + }; + + // An example component which references schema from the configuration and can be recursive through it. + const containerComponent: MyAppComponent = { + itemTypes: (lazyConfig: () => MyAppConfigPartial): Component.LazyArray => [ + () => createContainer(lazyConfig()), + ], + }; + function createContainer(config: MyAppConfigPartial): ItemSchema { + class Container extends sf.array("Container", config.allowedItemTypes) {} + class ContainerItem extends sf.object("ContainerItem", { + ...itemFields, + container: Container, + }) { + public static readonly description = "Text"; + public static default(): TextItem { + return new TextItem({ text: "", location: { x: 0, y: 0 } }); + } + + public foo(): void {} + } + + return ContainerItem; + } + + const appConfig = composeComponents([containerComponent, textComponent]); + + const treeConfig = new TreeViewConfiguration({ + schema: appConfig.allowedItemTypes, + enableSchemaValidation: true, + preventAmbiguity: true, + }); + }); + }); +}); diff --git a/packages/dds/tree/src/test/simple-tree/api/integrationTests.spec.ts b/packages/dds/tree/src/test/simple-tree/api/integrationTests.spec.ts new file mode 100644 index 000000000000..e615fc6e20d3 --- /dev/null +++ b/packages/dds/tree/src/test/simple-tree/api/integrationTests.spec.ts @@ -0,0 +1,35 @@ +/*! + * Copyright (c) Microsoft Corporation and contributors. All rights reserved. + * Licensed under the MIT License. + */ + +import { strict as assert } from "node:assert"; + +import { SchemaFactory } from "../../../simple-tree/index.js"; +import type { + ValidateRecursiveSchema, + // eslint-disable-next-line import/no-internal-modules +} from "../../../simple-tree/api/schemaFactoryRecursive.js"; +import { validateUsageError } from "../../utils.js"; + +const sf = new SchemaFactory("integration"); + +describe("simple-tree API integration tests", () => { + // TODO: this case should produce a usage error. + // Depending on where the error is detected, tests for recursive maps, arrays and co-recursive cases may be needed. + it.skip("making a recursive unhydrated object node errors", () => { + class O extends sf.objectRecursive("O", { + recursive: sf.optionalRecursive([() => O]), + }) {} + { + type _check = ValidateRecursiveSchema; + } + const obj = new O({ recursive: undefined }); + assert.throws( + () => { + obj.recursive = obj; + }, + validateUsageError(/recursive/), + ); + }); +}); diff --git a/packages/dds/tree/src/test/simple-tree/api/schemaFactory.examples.spec.ts b/packages/dds/tree/src/test/simple-tree/api/schemaFactory.examples.spec.ts index 5f0b490912cf..9e4a2a47c0d0 100644 --- a/packages/dds/tree/src/test/simple-tree/api/schemaFactory.examples.spec.ts +++ b/packages/dds/tree/src/test/simple-tree/api/schemaFactory.examples.spec.ts @@ -14,8 +14,17 @@ import { treeNodeApi as Tree, TreeViewConfiguration, type TreeView, + customizeSchemaTyping, + type GetTypes, + type Customizer, } from "../../../simple-tree/index.js"; import { TreeFactory } from "../../../treeFactory.js"; +import { + brand, + type areSafelyAssignable, + type Brand, + type requireTrue, +} from "../../../util/index.js"; // Since this no longer follows the builder pattern, it is a SchemaFactory instead of a SchemaBuilder. const schema = new SchemaFactory("com.example"); @@ -110,7 +119,7 @@ describe("Class based end to end example", () => { }); // Confirm that the alternative syntax for initialTree from the example above actually works. - it("using a mix of insertable content and nodes", () => { + it("using a mix of insertible content and nodes", () => { const factory = new TreeFactory({}); const theTree = factory.create( new MockFluidDataStoreRuntime({ idCompressor: createIdCompressor() }), @@ -127,4 +136,115 @@ describe("Class based end to end example", () => { }), ); }); + + it("customized narrowing", () => { + class Specific extends schema.object("Specific", { + s: customizeSchemaTyping(schema.string).simplified<"foo" | "bar">(), + }) {} + const parent = new Specific({ s: "bar" }); + // Reading field gives narrowed type + const s: "foo" | "bar" = parent.s; + + // @ts-expect-error custom typing violation does not build, but runs without error + const invalid = new Specific({ s: "x" }); + }); + + it("customized narrowing - safer", () => { + const specialString = customizeSchemaTyping(schema.string).custom<{ + input: "foo" | "bar"; + // Assignment can't be made be more restrictive than the read type, but we can choose to disable it. + readWrite: never; + }>(); + class Specific extends schema.object("Specific", { + s: specialString, + }) {} + const parent = new Specific({ s: "bar" }); + // Reading gives string + const s = parent.s; + type _check = requireTrue>; + + // @ts-expect-error Assigning is disabled; + parent.s = "x"; + + // @ts-expect-error custom typing violation does not build, but runs without error + const invalid = new Specific({ s: "x" }); + + class Array extends schema.array("Specific", specialString) {} + + // Array constructor is also narrowed correctly. + const a = new Array(["bar"]); + // Array insertion is narrowed as well. + a.insertAtEnd("bar"); + // and reading just gives string, since this example choose to do so since other clients could set unexpected strings as its not enforced by schema: + const s2 = a[0]; + type _check2 = requireTrue>; + }); + + it("customized branding", () => { + type SpecialString = Brand; + + class Specific extends schema.object("Specific", { + s: customizeSchemaTyping(schema.string).simplified(), + }) {} + const parent = new Specific({ s: brand("bar") }); + const s: SpecialString = parent.s; + + // @ts-expect-error custom typing violation does not build, but runs without error + const invalid = new Specific({ s: "x" }); + }); + + it("relaxed union", () => { + const runtimeDeterminedSchema = schema.string as + | typeof schema.string + | typeof schema.number; + class Strict extends schema.object("Strict", { + s: runtimeDeterminedSchema, + }) {} + + class Relaxed extends schema.object("Relaxed", { + s: customizeSchemaTyping(runtimeDeterminedSchema).relaxed(), + }) {} + + class RelaxedArray extends schema.object("Relaxed", { + s: customizeSchemaTyping([runtimeDeterminedSchema]).relaxed(), + }) {} + + const customizer = customizeSchemaTyping(runtimeDeterminedSchema); + { + const field = customizer.relaxed(); + type Field = typeof field; + type X = GetTypes; + } + + { + const field = customizeSchemaTyping(runtimeDeterminedSchema).relaxed(); + type Field = typeof field; + type X = GetTypes; + } + + const customizerArray = customizeSchemaTyping([runtimeDeterminedSchema]); + { + const field = customizerArray.relaxed(); + type Field = typeof field; + type X = GetTypes["input"]; + } + + type XXX = GetTypes; + + type F2 = GetTypes>; + type X2 = GetTypes["relaxed"]>>; + + // @ts-expect-error custom typing violation does not build, but runs without error + const s = new Strict({ s: "x" }); + // @ts-expect-error custom typing violation does not build, but runs without error + s.s = "Y"; + + const r = new Relaxed({ s: "x" }); + r.s = "Y"; + const ra = new RelaxedArray({ s: "x" }); + ra.s = "Y"; + + // @ts-expect-error custom typing violation does not build, but runs without error + const invalid = new Strict({ s: "x" }); + }); }); diff --git a/packages/dds/tree/src/test/simple-tree/api/schemaFactory.spec.ts b/packages/dds/tree/src/test/simple-tree/api/schemaFactory.spec.ts index e666ca6d8648..02856de5dbda 100644 --- a/packages/dds/tree/src/test/simple-tree/api/schemaFactory.spec.ts +++ b/packages/dds/tree/src/test/simple-tree/api/schemaFactory.spec.ts @@ -41,6 +41,7 @@ import { // eslint-disable-next-line import/no-internal-modules } from "../../../simple-tree/api/schemaFactory.js"; import type { + DefaultTreeNodeFromImplicitAllowedTypes, NodeFromSchema, TreeFieldFromImplicitField, TreeNodeFromImplicitAllowedTypes, @@ -92,6 +93,46 @@ import { validateUsageError } from "../../utils.js"; type FromArray = TreeNodeFromImplicitAllowedTypes<[typeof Note, typeof Note]>; type _check5 = requireTrue>; } + // TreeNodeFromImplicitAllowedTypes class + { + class NoteCustomized extends schema.object("Note", { text: schema.string }) { + public test: boolean = false; + } + + type _check = requireAssignableTo; + type _checkNodeType = requireAssignableTo< + typeof NoteCustomized, + TreeNodeSchema + >; + + type TestDefault = DefaultTreeNodeFromImplicitAllowedTypes; + + type _checkDefault1 = requireAssignableTo; + type _checkDefault2 = requireTrue>; + type Instance = InstanceType; + type _checkInstance = requireTrue>; + + type Test = TreeNodeFromImplicitAllowedTypes; + type _check2 = requireTrue>; + + type _check3 = requireTrue< + areSafelyAssignable< + TreeNodeFromImplicitAllowedTypes<[typeof NoteCustomized]>, + NoteCustomized + > + >; + type _check4 = requireTrue< + areSafelyAssignable< + TreeNodeFromImplicitAllowedTypes<[() => typeof NoteCustomized]>, + NoteCustomized + > + >; + + type FromArray = TreeNodeFromImplicitAllowedTypes< + [typeof NoteCustomized, typeof NoteCustomized] + >; + type _check5 = requireTrue>; + } // TreeFieldFromImplicitField { diff --git a/packages/dds/tree/src/test/simple-tree/api/schemaFactoryRecursive.spec.ts b/packages/dds/tree/src/test/simple-tree/api/schemaFactoryRecursive.spec.ts index c960e1ac3e2f..859ebea630bc 100644 --- a/packages/dds/tree/src/test/simple-tree/api/schemaFactoryRecursive.spec.ts +++ b/packages/dds/tree/src/test/simple-tree/api/schemaFactoryRecursive.spec.ts @@ -23,17 +23,21 @@ import { type ApplyKindInput, type NodeBuilderData, SchemaFactoryAlpha, + customizeSchemaTyping, + type AssignableTreeFieldFromImplicitField, + type ObjectFromSchemaRecord, } from "../../../simple-tree/index.js"; import type { ValidateRecursiveSchema, // eslint-disable-next-line import/no-internal-modules } from "../../../simple-tree/api/schemaFactoryRecursive.js"; -import type { - FieldSchemaUnsafe, - InsertableTreeFieldFromImplicitFieldUnsafe, - InsertableTreeNodeFromImplicitAllowedTypesUnsafe, - TreeFieldFromImplicitFieldUnsafe, - TreeNodeFromImplicitAllowedTypesUnsafe, +import { + customizeSchemaTypingUnsafe, + type FieldSchemaUnsafe, + type InsertableTreeFieldFromImplicitFieldUnsafe, + type InsertableTreeNodeFromImplicitAllowedTypesUnsafe, + type TreeFieldFromImplicitFieldUnsafe, + type TreeNodeFromImplicitAllowedTypesUnsafe, // eslint-disable-next-line import/no-internal-modules } from "../../../simple-tree/api/typesUnsafe.js"; import { TreeFactory } from "../../../treeFactory.js"; @@ -711,4 +715,117 @@ describe("SchemaFactory Recursive methods", () => { const r = hydrate(Root, { r: new ArrayRecursive([]) }); assert.deepEqual([...r.r], []); }); + + describe("custom types", () => { + it("custom non-recursive children", () => { + class O extends sf.objectRecursive("O", { + a: customizeSchemaTyping(sf.number).custom<{ + input: 1; + readWrite: never; + output: 2; + }>(), + recursive: sf.optionalRecursive([() => O]), + }) {} + + { + type _check = ValidateRecursiveSchema; + } + const obj = new O({ a: 1 }); + const read = obj.a; + type _checkRead = requireAssignableTo; + + // @ts-expect-error Readonly. + obj.a = 2 as never; + }); + + it("custom recursive children", () => { + class O extends sf.objectRecursive("O", { + // Test that customizeSchemaTyping works for non recursive members of recursive types + a: customizeSchemaTyping(sf.number).custom<{ + input: 1; + readWrite: never; + output: 2; + }>(), + recursive: sf.optionalRecursive( + customizeSchemaTypingUnsafe([() => O]).custom<{ + input: unknown; + readWrite: never; + }>(), + ), + }) {} + { + type _check = ValidateRecursiveSchema; + } + // Check custom typing applies to "a" and "recursive" + const obj = new O({ a: 1, recursive: undefined as unknown }); + const read = obj.recursive; + type _checkRead = requireAssignableTo; + + // @ts-expect-error Readonly. + obj.recursive = new O({ a: 1 }); + + // Readonly fails to apply apply when using FieldSchema on recursive objects. + obj.recursive = undefined; + // @ts-expect-error Readonly. + obj.a = 1; + + { + type Obj = ObjectFromSchemaRecord; + type A = AssignableTreeFieldFromImplicitField; + // eslint-disable-next-line @typescript-eslint/consistent-type-assertions + const x: Obj = {} as O; + // @ts-expect-error Readonly. + x.recursive = undefined; + } + + { + type A = AssignableTreeFieldFromImplicitField; + // eslint-disable-next-line @typescript-eslint/consistent-type-assertions + const x: O = {} as O; + // Readonly fails to apply apply when using FieldSchema on recursive objects. + x.recursive = undefined; + } + }); + + it("readonly fields", () => { + class O extends sf.objectRecursive("O", { + // Test that customizeSchemaTyping works for non recursive members of recursive types + opt: sf.optional( + customizeSchemaTyping(sf.number).custom<{ + readWrite: never; + }>(), + ), + req: sf.required( + customizeSchemaTyping(sf.number).custom<{ + readWrite: never; + }>(), + ), + recursive: sf.optionalRecursive( + customizeSchemaTypingUnsafe([() => O]).custom<{ + readWrite: never; + }>(), + ), + }) {} + { + type _check = ValidateRecursiveSchema; + } + // Check custom typing applies to "a" and "recursive" + const obj = new O({ req: 1 }); + const read = obj.recursive; + type _checkRead = requireAssignableTo; + + // @ts-expect-error Readonly. + obj.opt = 1; + // Ideally this would be an error as well, butt adding logic to do so breaks recursive type compilation when using it. + obj.opt = undefined; + + // @ts-expect-error Readonly. + obj.req = 1; + + assert.throws(() => { + // @ts-expect-error required. + obj.req = undefined; + }); + }); + }); }); diff --git a/packages/dds/tree/src/test/simple-tree/api/typesUnsafe.spec.ts b/packages/dds/tree/src/test/simple-tree/api/typesUnsafe.spec.ts index 48e53c75dedd..30de5a4ec694 100644 --- a/packages/dds/tree/src/test/simple-tree/api/typesUnsafe.spec.ts +++ b/packages/dds/tree/src/test/simple-tree/api/typesUnsafe.spec.ts @@ -3,12 +3,64 @@ * Licensed under the MIT License. */ -// eslint-disable-next-line import/no-internal-modules -import type { ReadonlyMapInlined } from "../../../simple-tree/api/typesUnsafe.js"; +import { + customizeSchemaTypingUnsafe, + type InsertableObjectFromSchemaRecordUnsafe, + type InsertableTreeFieldFromImplicitFieldUnsafe, + type InsertableTreeNodeFromImplicitAllowedTypesUnsafe, + type ReadonlyMapInlined, + // eslint-disable-next-line import/no-internal-modules +} from "../../../simple-tree/api/typesUnsafe.js"; +import { SchemaFactory, type ValidateRecursiveSchema } from "../../../simple-tree/index.js"; // eslint-disable-next-line import/no-internal-modules import type { numberSchema } from "../../../simple-tree/leafNodeSchema.js"; import type { areSafelyAssignable, requireTrue } from "../../../util/index.js"; -type MapInlined = ReadonlyMapInlined; +{ + type MapInlined = ReadonlyMapInlined; + type _check = requireTrue>>; +} + +// customizeSchemaTypingUnsafe and InsertableObjectFromSchemaRecordUnsafe +{ + const sf = new SchemaFactory("recursive"); + class Bad extends sf.objectRecursive("O", { + // customizeSchemaTypingUnsafe needs to be applied to the allowed types, not eh field: this is wrong! + recursive: customizeSchemaTypingUnsafe(sf.optionalRecursive([() => Bad])).custom<{ + input: 5; + }>(), + }) {} + + { + // Ideally this would error, but detecting this is invalid is hard. + type _check = ValidateRecursiveSchema; + } + + class O extends sf.objectRecursive("O", { + recursive: sf.optionalRecursive( + customizeSchemaTypingUnsafe([() => O]).custom<{ + input: 5; + }>(), + ), + }) {} + + // Record + { + type T = InsertableObjectFromSchemaRecordUnsafe["recursive"]; + type _check = requireTrue>; + } + + // Field + { + type T = InsertableTreeFieldFromImplicitFieldUnsafe; + type _check = requireTrue>; + } -type _check = requireTrue>>; + // AllowedTypes + { + type T = InsertableTreeNodeFromImplicitAllowedTypesUnsafe< + typeof O.info.recursive.allowedTypes + >; + type _check = requireTrue>; + } +} diff --git a/packages/dds/tree/src/test/simple-tree/core/types.spec.ts b/packages/dds/tree/src/test/simple-tree/core/types.spec.ts index 0ecfc646aaf3..b002986820c0 100644 --- a/packages/dds/tree/src/test/simple-tree/core/types.spec.ts +++ b/packages/dds/tree/src/test/simple-tree/core/types.spec.ts @@ -313,7 +313,7 @@ describe("simple-tree types", () => { protected static override oneTimeSetup(this: typeof TreeNodeValid): Context { log.push(this.name); - return getUnhydratedContext(A); + return getUnhydratedContext(this as typeof A); } } diff --git a/packages/dds/tree/src/test/simple-tree/flexList.spec.ts b/packages/dds/tree/src/test/simple-tree/flexList.spec.ts index d2f8df62a74e..31f3c70235db 100644 --- a/packages/dds/tree/src/test/simple-tree/flexList.spec.ts +++ b/packages/dds/tree/src/test/simple-tree/flexList.spec.ts @@ -6,11 +6,9 @@ import { strict as assert } from "node:assert"; import { - type FlexList, type FlexListToUnion, isLazy, markEager, - normalizeFlexListEager, // Allow importing from this specific file which is being tested: /* eslint-disable-next-line import/no-internal-modules */ } from "../../simple-tree/flexList.js"; @@ -29,23 +27,10 @@ import type { areSafelyAssignable, requireTrue } from "../../util/index.js"; } describe("FlexList", () => { - it("correctly normalizes lists to be eager", () => { - const list = [2, (): 1 => 1] as const; - const normalized: readonly (1 | 2)[] = normalizeFlexListEager(list); - assert.deepEqual(normalized, [2, 1]); - }); - it("can mark functions as eager", () => { const fn = () => 42; assert.equal(isLazy(fn), true); markEager(fn); assert.equal(isLazy(fn), false); }); - - it("correctly normalizes functions marked as eager", () => { - const eagerGenerator = markEager(() => 42); - const lazyGenerator = () => () => 42; - const list: FlexList<() => number> = [eagerGenerator, lazyGenerator]; - normalizeFlexListEager(list).forEach((g) => assert.equal(g(), 42)); - }); }); diff --git a/packages/dds/tree/src/test/simple-tree/objectNode.spec.ts b/packages/dds/tree/src/test/simple-tree/objectNode.spec.ts index 46e7ec2fcee3..cbc5fa5df013 100644 --- a/packages/dds/tree/src/test/simple-tree/objectNode.spec.ts +++ b/packages/dds/tree/src/test/simple-tree/objectNode.spec.ts @@ -11,7 +11,9 @@ import { SchemaFactory, typeNameSymbol, typeSchemaSymbol, + type GetTypesUnsafe, type NodeBuilderData, + type TreeNode, } from "../../simple-tree/index.js"; import type { InsertableObjectFromSchemaRecord, @@ -20,15 +22,20 @@ import type { import { describeHydration, hydrate, pretty } from "./utils.js"; import type { areSafelyAssignable, + FlattenKeys, requireAssignableTo, requireTrue, } from "../../util/index.js"; import { validateUsageError } from "../utils.js"; import { Tree } from "../../shared-tree/index.js"; import type { + ImplicitFieldSchema, + InsertableField, InsertableTreeFieldFromImplicitField, InsertableTreeNodeFromAllowedTypes, InsertableTypedNode, + TreeFieldFromImplicitField, + TreeLeafValue, // eslint-disable-next-line import/no-internal-modules } from "../../simple-tree/schemaTypes.js"; @@ -234,10 +241,130 @@ describeHydration( }) {} const n = init(HasId, {}); assert.throws(() => { - // TODO: AB:9129: this should not compile + // @ts-expect-error this should not compile n.id = "x"; }); }); + + it("assigning non-exact schema errors - ImplicitFieldSchema", () => { + const child: ImplicitFieldSchema = schemaFactory.number; + class NonExact extends schemaFactory.object("NonExact", { + child, + }) {} + // @ts-expect-error Should not compile, and does not due to non-exact typing. + const initial: InsertableField = { child: 1 }; + const n: NonExact = init(NonExact, initial); + assert.throws(() => { + // @ts-expect-error this should not compile + n.child = "x"; + }); + }); + + it("assigning non-exact optional schema", () => { + const child: ImplicitFieldSchema = schemaFactory.number; + class NonExact extends schemaFactory.object("NonExact", { + child: schemaFactory.optional(child), + }) {} + // @ts-expect-error Should not compile, and does not due to non-exact typing. + const initial: InsertableField = { child: 1 }; + const n: NonExact = init(NonExact, initial); + + assert.throws(() => { + // @ts-expect-error this should not compile + n.child = "x"; + }); + + type Read = TreeFieldFromImplicitField<(typeof NonExact.info)["child"]>; + type _check1 = requireTrue< + areSafelyAssignable + >; + + const read = n.child; + type _check2 = requireTrue< + areSafelyAssignable + >; + + // This would be ok, but allowing it forces allowing assigning any of the values that can be read, which is very unsafe here. + // @ts-expect-error this should not compile + n.child = undefined; + }); + + it("assigning non-exact schema errors - union", () => { + const child = schemaFactory.number as + | typeof schemaFactory.number + | typeof schemaFactory.null; + class NonExact extends schemaFactory.object("NonExact", { + child, + }) {} + // @ts-expect-error Should not compile, and does not due to non-exact typing. + const initial: InsertableField = { child: 1 }; + const n: NonExact = init(NonExact, initial); + const childRead = n.child; + assert.throws(() => { + // @ts-expect-error this should not compile + n.child = "x"; + }); + + assert.throws(() => { + // @ts-expect-error this should not compile + n.child = null; + }); + + // @ts-expect-error this should not compile + n.child = 5; + }); + + it("assigning identifier errors - ImplicitFieldSchema - recursive", () => { + class HasId extends schemaFactory.objectRecursive("hasID", { + id: schemaFactory.identifier, + }) {} + const n = init(HasId, {}); + assert.throws(() => { + // @ts-expect-error Readonly + n.id = "x"; + }); + }); + + it("assigning non-exact schema errors - union - recursive", () => { + const child: ImplicitFieldSchema = schemaFactory.number; + class NonExact extends schemaFactory.objectRecursive("NonExact", { + child, + }) {} + // @ts-expect-error Should not compile, and does not due to non-exact typing. + const initial: InsertableField = { child: 1 }; + const n: NonExact = init(NonExact, initial); + assert.throws(() => { + // Due to recursive type limitations, this compiles but shouldn't, see ObjectFromSchemaRecordUnsafe + n.child = "x"; + }); + }); + + it("assigning non-exact schema errors - recursive", () => { + const child = schemaFactory.number as + | typeof schemaFactory.number + | typeof schemaFactory.null; + class NonExact extends schemaFactory.objectRecursive("NonExact", { + child, + }) {} + // @ts-expect-error Should not compile, and does not due to non-exact typing. + const initial: InsertableField = { child: 1 }; + const n: NonExact = init(NonExact, initial); + const childRead = n.child; + type XXX = FlattenKeys>; + type _check = requireTrue>; + assert.throws(() => { + // @ts-expect-error this should not compile + n.child = "x"; + }); + + assert.throws(() => { + // Due to recursive type limitations, this compiles but shouldn't, see ObjectFromSchemaRecordUnsafe + n.child = null; + }); + + // Due to recursive type limitations, this compiles but shouldn't, see ObjectFromSchemaRecordUnsafe + n.child = 5; + }); }); // Regression test for accidental use of ?? preventing null values from being read correctly. @@ -446,7 +573,8 @@ describeHydration( foo: schemaFactory.optional(schemaFactory.number), }) { // Since fields are own properties, we expect inherited properties (like this) to be shadowed by fields. - // However in TypeScript they work like inherited properties, so the types don't make the runtime behavior. + // However in TypeScript they work like inherited properties, so the types don't match the runtime behavior. + // @ts-expect-error bad shadow // eslint-disable-next-line @typescript-eslint/class-literal-property-style public override get foo(): 5 { return 5; diff --git a/packages/dds/tree/src/test/simple-tree/schemaTypes.spec.ts b/packages/dds/tree/src/test/simple-tree/schemaTypes.spec.ts index 2e6fb0d55938..ea8a7ab41e7e 100644 --- a/packages/dds/tree/src/test/simple-tree/schemaTypes.spec.ts +++ b/packages/dds/tree/src/test/simple-tree/schemaTypes.spec.ts @@ -19,19 +19,23 @@ import { } from "../../simple-tree/index.js"; import { type AllowedTypes, + type CustomizedSchemaTyping, + type DefaultInsertableTreeNodeFromImplicitAllowedTypes, + type DefaultTreeNodeFromImplicitAllowedTypes, type FieldSchema, type ImplicitAllowedTypes, type ImplicitFieldSchema, type InsertableField, type InsertableTreeFieldFromImplicitField, type InsertableTreeNodeFromAllowedTypes, - type InsertableTreeNodeFromImplicitAllowedTypes, type InsertableTypedNode, type NodeBuilderData, type NodeFromSchema, + type SchemaUnionToIntersection, type TreeFieldFromImplicitField, type TreeLeafValue, type TreeNodeFromImplicitAllowedTypes, + type UnsafeUnknownSchema, areImplicitFieldSchemaEqual, normalizeAllowedTypes, // eslint-disable-next-line import/no-internal-modules @@ -75,6 +79,24 @@ describe("schemaTypes", () => { >; } + // CustomSchemaIntersection + { + type Original = A | B; + type Custom = CustomizedSchemaTyping; + type OriginalIntersection = UnionToIntersection; + type CustomIntersection = UnionToIntersection; + + type OriginalSchemaIntersection = SchemaUnionToIntersection; + type CustomSchemaIntersection = SchemaUnionToIntersection; + + type _check1 = requireTrue>; + type _check2 = requireTrue>; + type _check3 = requireTrue>; + type _check4 = requireTrue< + areSafelyAssignable + >; + } + // InsertableTreeFieldFromImplicitField { // Input @@ -104,22 +126,24 @@ describe("schemaTypes", () => { type _check8 = requireTrue>; } - // InsertableTreeNodeFromImplicitAllowedTypes + // DefaultInsertableTreeNodeFromImplicitAllowedTypes { // Input - type I3 = InsertableTreeNodeFromImplicitAllowedTypes; - type I4 = InsertableTreeNodeFromImplicitAllowedTypes; - type I5 = InsertableTreeNodeFromImplicitAllowedTypes< + type I3 = DefaultInsertableTreeNodeFromImplicitAllowedTypes; + type I4 = DefaultInsertableTreeNodeFromImplicitAllowedTypes; + type I5 = DefaultInsertableTreeNodeFromImplicitAllowedTypes< typeof numberSchema | typeof stringSchema >; - type I8 = InsertableTreeNodeFromImplicitAllowedTypes; + type I8 = DefaultInsertableTreeNodeFromImplicitAllowedTypes; - type I6 = InsertableTreeNodeFromImplicitAllowedTypes< + type I6 = DefaultInsertableTreeNodeFromImplicitAllowedTypes< typeof numberSchema & typeof stringSchema >; - type I7 = InsertableTreeNodeFromImplicitAllowedTypes; + type I7 = DefaultInsertableTreeNodeFromImplicitAllowedTypes< + AllowedTypes & TreeNodeSchema + >; - type I9 = InsertableTreeNodeFromImplicitAllowedTypes; + type I9 = DefaultInsertableTreeNodeFromImplicitAllowedTypes; // These types should behave contravariantly type _check3 = requireTrue>; @@ -128,19 +152,19 @@ describe("schemaTypes", () => { type _check6 = requireTrue>; // Actual schema unions - type I12 = InsertableTreeNodeFromImplicitAllowedTypes; + type I12 = DefaultInsertableTreeNodeFromImplicitAllowedTypes; type _check12 = requireTrue>; - type I10 = InsertableTreeNodeFromImplicitAllowedTypes<[typeof numberSchema]>; + type I10 = DefaultInsertableTreeNodeFromImplicitAllowedTypes<[typeof numberSchema]>; type _check10 = requireTrue>; - type I11 = InsertableTreeNodeFromImplicitAllowedTypes< + type I11 = DefaultInsertableTreeNodeFromImplicitAllowedTypes< [typeof numberSchema, typeof stringSchema] >; type _check11 = requireTrue>; // boolean // boolean is sometimes a union of true and false, so it can break in its owns special ways - type I13 = InsertableTreeNodeFromImplicitAllowedTypes; + type I13 = DefaultInsertableTreeNodeFromImplicitAllowedTypes; type _check13 = requireTrue>; } @@ -219,6 +243,40 @@ describe("schemaTypes", () => { type _check13 = requireTrue>; } + // DefaultTreeNodeFromImplicitAllowedTypes + { + class Simple extends schema.object("A", { x: [schema.number] }) {} + class Customized extends schema.object("B", { x: [schema.number] }) { + public customized = true; + } + + type TA = DefaultTreeNodeFromImplicitAllowedTypes; + type _checkA = requireAssignableTo; + + type TB = DefaultTreeNodeFromImplicitAllowedTypes; + type _checkB = requireAssignableTo; + } + + // Example CustomTypes + + /** + * Ignores schema, and allows any edit at compile time. + */ + interface AnyTypes { + input: InsertableField; + readWrite: TreeNode | TreeLeafValue; + output: TreeNode | TreeLeafValue; + } + + /** + * Ignores schema, forbidding all edits. + */ + interface UnknownTypes { + input: never; + readWrite: never; + output: TreeNode | TreeLeafValue; + } + // NodeFromSchema { class Simple extends schema.object("A", { x: [schema.number] }) {} diff --git a/packages/dds/tree/src/test/types/validateTreePrevious.generated.ts b/packages/dds/tree/src/test/types/validateTreePrevious.generated.ts index a26c71f00651..160c688e5e8e 100644 --- a/packages/dds/tree/src/test/types/validateTreePrevious.generated.ts +++ b/packages/dds/tree/src/test/types/validateTreePrevious.generated.ts @@ -517,6 +517,7 @@ declare type old_as_current_for_TypeAlias_InsertableTreeNodeFromImplicitAllowedT * typeValidation.broken: * "TypeAlias_InsertableTreeNodeFromImplicitAllowedTypes": {"backCompat": false} */ +// @ts-expect-error compatibility expected to be broken declare type current_as_old_for_TypeAlias_InsertableTreeNodeFromImplicitAllowedTypes = requireAssignableTo>, TypeOnly>> /* diff --git a/packages/dds/tree/src/test/util/typeUtils.spec.ts b/packages/dds/tree/src/test/util/typeUtils.spec.ts index 0d9a99ebd81f..d8b83ba1bc96 100644 --- a/packages/dds/tree/src/test/util/typeUtils.spec.ts +++ b/packages/dds/tree/src/test/util/typeUtils.spec.ts @@ -89,4 +89,42 @@ import type { areSafelyAssignable, { x: 2 }> >; type _check4 = requireTrue, 1>>; + + type _check5 = requireTrue< + areSafelyAssignable, readonly [1 | 2]> + >; + + { + type intersectedUnion = ({ a: 1 } | { b: 2 }) & { foo: 1 }; + type convertedx = UnionToIntersection; + type converted = UnionToIntersection2; + type _check6 = requireTrue>; + + type UnionToIntersection2 = T extends T ? T : never; + + type _check8 = requireTrue< + areSafelyAssignable< + ({ a: 1 } | { b: 2 }) & { foo: 1 }, + ({ a: 1 } & { foo: 1 }) | ({ b: 2 } & { foo: 1 }) + > + >; + + type _check9 = requireTrue< + areSafelyAssignable< + ({ a: 1 } | { a: 2 }) & { foo: 1 }, + ({ a: 1 } & { foo: 1 }) | ({ a: 2 } & { foo: 1 }) + > + >; + + type _check10 = requireTrue< + areSafelyAssignable< + { a: 1 | 2 } & { foo: 1 }, + ({ a: 1 } & { foo: 1 }) | ({ a: 2 } & { foo: 1 }) + > + >; + } + + type _check7 = requireTrue< + areSafelyAssignable, { a: 1 } & { b: 2 }> + >; } diff --git a/packages/framework/fluid-framework/api-report/fluid-framework.alpha.api.md b/packages/framework/fluid-framework/api-report/fluid-framework.alpha.api.md index 781ffdf8bfc7..3645afa1edcb 100644 --- a/packages/framework/fluid-framework/api-report/fluid-framework.alpha.api.md +++ b/packages/framework/fluid-framework/api-report/fluid-framework.alpha.api.md @@ -28,11 +28,22 @@ type ApplyKind = { [FieldKind.Identifier]: T; }[Kind]; +// @public +export type ApplyKindAssignment = [Kind] extends [ +FieldKind.Required +] ? T : [Kind] extends [FieldKind.Optional] ? T | undefined : never; + // @public type ApplyKindInput = [ Kind ] extends [FieldKind.Required] ? T : [Kind] extends [FieldKind.Optional] ? T | undefined : [Kind] extends [FieldKind.Identifier] ? DefaultsAreOptional extends true ? T | undefined : T : never; +// @public +export type AssignableTreeFieldFromImplicitField> = [TSchema] extends [FieldSchema] ? ApplyKindAssignment["readWrite"], Kind> : [TSchema] extends [ImplicitAllowedTypes] ? GetTypes["readWrite"] : never; + +// @public +export type AssignableTreeFieldFromImplicitFieldUnsafe> = TSchema extends FieldSchemaUnsafe ? ApplyKindAssignment["readWrite"], Kind> : GetTypesUnsafe["readWrite"]; + // @alpha export function asTreeViewAlpha(view: TreeView): TreeViewAlpha; @@ -67,6 +78,13 @@ export interface CommitMetadata { // @alpha export function comparePersistedSchema(persisted: JsonCompatible, view: ImplicitFieldSchema, options: ICodecOptions, canInitialize: boolean): SchemaCompatibilityStatus; +// @alpha +export namespace Component { + export type ComponentSchemaCollection = (lazyConfiguration: () => TConfig) => LazyArray; + export function composeComponentSchema(allComponents: readonly ComponentSchemaCollection[], lazyConfiguration: () => TConfig): (() => TItem)[]; + export type LazyArray = readonly (() => T)[]; +} + // @alpha export type ConciseTree = Exclude | THandle | ConciseTree[] | { [key: string]: ConciseTree; @@ -118,10 +136,66 @@ export function createSimpleTreeIndex(view: TreeView, indexer: Map, getValue: (nodes: TreeIndexNodes>) => TValue, isKeyValid: (key: TreeIndexKey) => key is TKey, indexableSchema: readonly TSchema[]): SimpleTreeIndex; +// @public +export type CustomizedSchemaTyping = TSchema & { + [CustomizedTyping]: TCustom; +}; + +// @public +export const CustomizedTyping: unique symbol; + +// @public +export type CustomizedTyping = typeof CustomizedTyping; + +// @alpha @sealed +export interface Customizer { + custom>(): CustomizedSchemaTyping[Property] : GetTypes[Property]; + }>; + relaxed(): CustomizedSchemaTyping : TSchema extends AllowedTypes ? TSchema[number] extends LazyItem ? InsertableTypedNode : never : never; + readWrite: TreeNodeFromImplicitAllowedTypes; + output: TreeNodeFromImplicitAllowedTypes; + }>; + simplified>(): CustomizedSchemaTyping; + simplifiedUnrestricted(): CustomizedSchemaTyping; + strict(): CustomizedSchemaTyping>; +} + +// @alpha +export function customizeSchemaTyping(schema: TSchema): Customizer; + +// @public @sealed +export interface CustomTypes { + readonly input: unknown; + readonly output: TreeLeafValue | TreeNode; + readonly readWrite: TreeLeafValue | TreeNode; +} + +// @public +export type DefaultInsertableTreeNodeFromImplicitAllowedTypes = [TSchema] extends [TreeNodeSchema] ? InsertableTypedNode : [TSchema] extends [AllowedTypes] ? InsertableTreeNodeFromAllowedTypes : never; + +// @public +export type DefaultInsertableTreeNodeFromImplicitAllowedTypesUnsafe> = [TSchema] extends [TreeNodeSchemaUnsafe] ? InsertableTypedNodeUnsafe : [TSchema] extends [AllowedTypesUnsafe] ? InsertableTreeNodeFromAllowedTypesUnsafe : never; + // @public @sealed interface DefaultProvider extends ErasedType<"@fluidframework/tree.FieldProvider"> { } +// @public +export type DefaultTreeNodeFromImplicitAllowedTypes = TSchema extends TreeNodeSchema ? NodeFromSchema : TSchema extends AllowedTypes ? NodeFromSchema> : unknown; + +// @public +export type DefaultTreeNodeFromImplicitAllowedTypesUnsafe> = TSchema extends TreeNodeSchemaUnsafe ? NodeFromSchemaUnsafe : TSchema extends AllowedTypesUnsafe ? NodeFromSchemaUnsafe> : unknown; + // @alpha export interface EncodeOptions { readonly useStoredKeys?: boolean; @@ -145,6 +219,9 @@ export abstract class ErasedType { protected abstract brand(dummy: never): Name; } +// @alpha +export function evaluateLazySchema(value: LazyItem): T; + // @public type ExtractItemType = Item extends () => infer Result ? Result : Item; @@ -260,6 +337,16 @@ export function getBranch(v // @alpha export function getJsonSchema(schema: ImplicitFieldSchema): JsonTreeSchema; +// @public +export type GetTypes = [TSchema] extends [ +CustomizedSchemaTyping +] ? TCustom : StrictTypes; + +// @public +export type GetTypesUnsafe> = [ +TSchema +] extends [CustomizedSchemaTyping] ? TCustom : StrictTypesUnsafe; + // @alpha export interface ICodecOptions { readonly jsonValidator: JsonValidator; @@ -547,7 +634,7 @@ type _InlineTrick = 0; export type Input = T; // @alpha -export type Insertable = TSchema extends ImplicitAllowedTypes ? InsertableTreeNodeFromImplicitAllowedTypes : InsertableContent; +export type Insertable = InsertableTreeNodeFromImplicitAllowedTypes; // @alpha export type InsertableContent = Unhydrated | FactoryContent; @@ -572,7 +659,7 @@ export type InsertableObjectFromSchemaRecordUnsafe> = [TSchema] extends [FieldSchema] ? ApplyKindInput, Kind, true> : [TSchema] extends [ImplicitAllowedTypes] ? InsertableTreeNodeFromImplicitAllowedTypes : never; +export type InsertableTreeFieldFromImplicitField] ? TSchemaInput : SchemaUnionToIntersection> = [TSchema] extends [FieldSchema] ? ApplyKindInput, Kind, true> : [TSchema] extends [ImplicitAllowedTypes] ? InsertableTreeNodeFromImplicitAllowedTypes : never; // @public export type InsertableTreeFieldFromImplicitFieldUnsafe, TSchema = UnionToIntersection> = [TSchema] extends [FieldSchemaUnsafe] ? ApplyKindInput, Kind, true> : [TSchema] extends [ImplicitAllowedTypes] ? InsertableTreeNodeFromImplicitAllowedTypesUnsafe : never; @@ -590,12 +677,10 @@ LazyItem, ] ? InsertableTypedNodeUnsafe | InsertableTreeNodeFromAllowedTypesUnsafe : never; // @public -export type InsertableTreeNodeFromImplicitAllowedTypes = [ -TSchema -] extends [TreeNodeSchema] ? InsertableTypedNode : [TSchema] extends [AllowedTypes] ? InsertableTreeNodeFromAllowedTypes : never; +export type InsertableTreeNodeFromImplicitAllowedTypes = GetTypes["input"]; // @public -export type InsertableTreeNodeFromImplicitAllowedTypesUnsafe> = [TSchema] extends [TreeNodeSchemaUnsafe] ? InsertableTypedNodeUnsafe : [TSchema] extends [AllowedTypesUnsafe] ? InsertableTreeNodeFromAllowedTypesUnsafe : never; +export type InsertableTreeNodeFromImplicitAllowedTypesUnsafe> = GetTypesUnsafe["input"]; // @public export type InsertableTypedNode> = (T extends TreeNodeSchema ? NodeBuilderData : never) | (T extends TreeNodeSchema ? Unhydrated ? never : NodeFromSchema> : never); @@ -886,12 +971,30 @@ export const noopValidator: JsonValidator; // @public type ObjectFromSchemaRecord> = { - -readonly [Property in keyof T]: Property extends string ? TreeFieldFromImplicitField : unknown; + -readonly [Property in keyof T as [ + AssignableTreeFieldFromImplicitField + ] extends [never | undefined] ? never : Property]: AssignableTreeFieldFromImplicitField; +} & { + readonly [Property in keyof T]: TreeFieldFromImplicitField; }; // @public type ObjectFromSchemaRecordUnsafe>> = { - -readonly [Property in keyof T]: TreeFieldFromImplicitFieldUnsafe; + -readonly [Property in keyof T as [T[Property]] extends [ + CustomizedSchemaTyping + ] ? never : Property]: AssignableTreeFieldFromImplicitFieldUnsafe; +} & { + readonly [Property in keyof T as [T[Property]] extends [ + CustomizedSchemaTyping + ] ? Property : never]: TreeFieldFromImplicitFieldUnsafe; }; // @public @@ -1092,6 +1195,11 @@ export const schemaStatics: { readonly requiredRecursive: (t: T_3, props?: Omit) => FieldSchemaUnsafe; }; +// @public +export type SchemaUnionToIntersection = [T] extends [ +CustomizedSchemaTyping +] ? T : UnionToIntersection; + // @alpha export interface SchemaValidationFunction { check(data: unknown): data is Static; @@ -1135,6 +1243,26 @@ export function singletonSchema, true, Record, undefined>; +// @public @sealed +export interface StrictTypes, TOutput extends TreeNode | TreeLeafValue = DefaultTreeNodeFromImplicitAllowedTypes> { + // (undocumented) + input: TInput; + // (undocumented) + output: TOutput; + // (undocumented) + readWrite: TInput extends never ? never : TOutput; +} + +// @public +export interface StrictTypesUnsafe, TInput = DefaultInsertableTreeNodeFromImplicitAllowedTypesUnsafe, TOutput = DefaultTreeNodeFromImplicitAllowedTypesUnsafe> { + // (undocumented) + input: TInput; + // (undocumented) + output: TOutput; + // (undocumented) + readWrite: TOutput; +} + // @public export interface Tagged { // (undocumented) @@ -1344,10 +1472,10 @@ export interface TreeNodeApi { } // @public -export type TreeNodeFromImplicitAllowedTypes = TSchema extends TreeNodeSchema ? NodeFromSchema : TSchema extends AllowedTypes ? NodeFromSchema> : unknown; +export type TreeNodeFromImplicitAllowedTypes = GetTypes["output"]; // @public -type TreeNodeFromImplicitAllowedTypesUnsafe> = TSchema extends TreeNodeSchemaUnsafe ? NodeFromSchemaUnsafe : TSchema extends AllowedTypesUnsafe ? NodeFromSchemaUnsafe> : unknown; +type TreeNodeFromImplicitAllowedTypesUnsafe> = GetTypesUnsafe["output"]; // @public @sealed export type TreeNodeSchema = (TNode extends TreeNode ? TreeNodeSchemaClass : never) | TreeNodeSchemaNonClass; diff --git a/packages/framework/fluid-framework/api-report/fluid-framework.beta.api.md b/packages/framework/fluid-framework/api-report/fluid-framework.beta.api.md index 6557b7dae1e3..0cf118c7a09f 100644 --- a/packages/framework/fluid-framework/api-report/fluid-framework.beta.api.md +++ b/packages/framework/fluid-framework/api-report/fluid-framework.beta.api.md @@ -17,11 +17,22 @@ type ApplyKind = { [FieldKind.Identifier]: T; }[Kind]; +// @public +export type ApplyKindAssignment = [Kind] extends [ +FieldKind.Required +] ? T : [Kind] extends [FieldKind.Optional] ? T | undefined : never; + // @public type ApplyKindInput = [ Kind ] extends [FieldKind.Required] ? T : [Kind] extends [FieldKind.Optional] ? T | undefined : [Kind] extends [FieldKind.Identifier] ? DefaultsAreOptional extends true ? T | undefined : T : never; +// @public +export type AssignableTreeFieldFromImplicitField> = [TSchema] extends [FieldSchema] ? ApplyKindAssignment["readWrite"], Kind> : [TSchema] extends [ImplicitAllowedTypes] ? GetTypes["readWrite"] : never; + +// @public +export type AssignableTreeFieldFromImplicitFieldUnsafe> = TSchema extends FieldSchemaUnsafe ? ApplyKindAssignment["readWrite"], Kind> : GetTypesUnsafe["readWrite"]; + // @public export enum AttachState { Attached = "Attached", @@ -70,10 +81,40 @@ export interface ContainerSchema { readonly initialObjects: Record; } +// @public +export type CustomizedSchemaTyping = TSchema & { + [CustomizedTyping]: TCustom; +}; + +// @public +export const CustomizedTyping: unique symbol; + +// @public +export type CustomizedTyping = typeof CustomizedTyping; + +// @public @sealed +export interface CustomTypes { + readonly input: unknown; + readonly output: TreeLeafValue | TreeNode; + readonly readWrite: TreeLeafValue | TreeNode; +} + +// @public +export type DefaultInsertableTreeNodeFromImplicitAllowedTypes = [TSchema] extends [TreeNodeSchema] ? InsertableTypedNode : [TSchema] extends [AllowedTypes] ? InsertableTreeNodeFromAllowedTypes : never; + +// @public +export type DefaultInsertableTreeNodeFromImplicitAllowedTypesUnsafe> = [TSchema] extends [TreeNodeSchemaUnsafe] ? InsertableTypedNodeUnsafe : [TSchema] extends [AllowedTypesUnsafe] ? InsertableTreeNodeFromAllowedTypesUnsafe : never; + // @public @sealed interface DefaultProvider extends ErasedType<"@fluidframework/tree.FieldProvider"> { } +// @public +export type DefaultTreeNodeFromImplicitAllowedTypes = TSchema extends TreeNodeSchema ? NodeFromSchema : TSchema extends AllowedTypes ? NodeFromSchema> : unknown; + +// @public +export type DefaultTreeNodeFromImplicitAllowedTypesUnsafe> = TSchema extends TreeNodeSchemaUnsafe ? NodeFromSchemaUnsafe : TSchema extends AllowedTypesUnsafe ? NodeFromSchemaUnsafe> : unknown; + // @public @sealed export abstract class ErasedType { static [Symbol.hasInstance](value: never): value is never; @@ -146,6 +187,16 @@ export type FluidObject = { // @public export type FluidObjectProviderKeys = string extends TProp ? never : number extends TProp ? never : TProp extends keyof Required[TProp] ? Required[TProp] extends Required[TProp]>[TProp] ? TProp : never : never; +// @public +export type GetTypes = [TSchema] extends [ +CustomizedSchemaTyping +] ? TCustom : StrictTypes; + +// @public +export type GetTypesUnsafe> = [ +TSchema +] extends [CustomizedSchemaTyping] ? TCustom : StrictTypesUnsafe; + // @public export interface IConnection { readonly id: string; @@ -431,7 +482,7 @@ export type InsertableObjectFromSchemaRecordUnsafe> = [TSchema] extends [FieldSchema] ? ApplyKindInput, Kind, true> : [TSchema] extends [ImplicitAllowedTypes] ? InsertableTreeNodeFromImplicitAllowedTypes : never; +export type InsertableTreeFieldFromImplicitField] ? TSchemaInput : SchemaUnionToIntersection> = [TSchema] extends [FieldSchema] ? ApplyKindInput, Kind, true> : [TSchema] extends [ImplicitAllowedTypes] ? InsertableTreeNodeFromImplicitAllowedTypes : never; // @public export type InsertableTreeFieldFromImplicitFieldUnsafe, TSchema = UnionToIntersection> = [TSchema] extends [FieldSchemaUnsafe] ? ApplyKindInput, Kind, true> : [TSchema] extends [ImplicitAllowedTypes] ? InsertableTreeNodeFromImplicitAllowedTypesUnsafe : never; @@ -449,12 +500,10 @@ LazyItem, ] ? InsertableTypedNodeUnsafe | InsertableTreeNodeFromAllowedTypesUnsafe : never; // @public -export type InsertableTreeNodeFromImplicitAllowedTypes = [ -TSchema -] extends [TreeNodeSchema] ? InsertableTypedNode : [TSchema] extends [AllowedTypes] ? InsertableTreeNodeFromAllowedTypes : never; +export type InsertableTreeNodeFromImplicitAllowedTypes = GetTypes["input"]; // @public -export type InsertableTreeNodeFromImplicitAllowedTypesUnsafe> = [TSchema] extends [TreeNodeSchemaUnsafe] ? InsertableTypedNodeUnsafe : [TSchema] extends [AllowedTypesUnsafe] ? InsertableTreeNodeFromAllowedTypesUnsafe : never; +export type InsertableTreeNodeFromImplicitAllowedTypesUnsafe> = GetTypesUnsafe["input"]; // @public export type InsertableTypedNode> = (T extends TreeNodeSchema ? NodeBuilderData : never) | (T extends TreeNodeSchema ? Unhydrated ? never : NodeFromSchema> : never); @@ -632,12 +681,30 @@ export interface NodeSchemaOptions { // @public type ObjectFromSchemaRecord> = { - -readonly [Property in keyof T]: Property extends string ? TreeFieldFromImplicitField : unknown; + -readonly [Property in keyof T as [ + AssignableTreeFieldFromImplicitField + ] extends [never | undefined] ? never : Property]: AssignableTreeFieldFromImplicitField; +} & { + readonly [Property in keyof T]: TreeFieldFromImplicitField; }; // @public type ObjectFromSchemaRecordUnsafe>> = { - -readonly [Property in keyof T]: TreeFieldFromImplicitFieldUnsafe; + -readonly [Property in keyof T as [T[Property]] extends [ + CustomizedSchemaTyping + ] ? never : Property]: AssignableTreeFieldFromImplicitFieldUnsafe; +} & { + readonly [Property in keyof T as [T[Property]] extends [ + CustomizedSchemaTyping + ] ? Property : never]: TreeFieldFromImplicitFieldUnsafe; }; // @public @@ -784,6 +851,11 @@ export const schemaStatics: { readonly requiredRecursive: (t: T_3, props?: Omit) => FieldSchemaUnsafe; }; +// @public +export type SchemaUnionToIntersection = [T] extends [ +CustomizedSchemaTyping +] ? T : UnionToIntersection; + // @public type ScopedSchemaName = TScope extends undefined ? `${TName}` : `${TScope}.${TName}`; @@ -795,6 +867,26 @@ export interface SharedObjectKind extends ErasedTyp // @public export const SharedTree: SharedObjectKind; +// @public @sealed +export interface StrictTypes, TOutput extends TreeNode | TreeLeafValue = DefaultTreeNodeFromImplicitAllowedTypes> { + // (undocumented) + input: TInput; + // (undocumented) + output: TOutput; + // (undocumented) + readWrite: TInput extends never ? never : TOutput; +} + +// @public +export interface StrictTypesUnsafe, TInput = DefaultInsertableTreeNodeFromImplicitAllowedTypesUnsafe, TOutput = DefaultTreeNodeFromImplicitAllowedTypesUnsafe> { + // (undocumented) + input: TInput; + // (undocumented) + output: TOutput; + // (undocumented) + readWrite: TOutput; +} + // @public export interface Tagged { // (undocumented) @@ -916,10 +1008,10 @@ export interface TreeNodeApi { } // @public -export type TreeNodeFromImplicitAllowedTypes = TSchema extends TreeNodeSchema ? NodeFromSchema : TSchema extends AllowedTypes ? NodeFromSchema> : unknown; +export type TreeNodeFromImplicitAllowedTypes = GetTypes["output"]; // @public -type TreeNodeFromImplicitAllowedTypesUnsafe> = TSchema extends TreeNodeSchemaUnsafe ? NodeFromSchemaUnsafe : TSchema extends AllowedTypesUnsafe ? NodeFromSchemaUnsafe> : unknown; +type TreeNodeFromImplicitAllowedTypesUnsafe> = GetTypesUnsafe["output"]; // @public @sealed export type TreeNodeSchema = (TNode extends TreeNode ? TreeNodeSchemaClass : never) | TreeNodeSchemaNonClass; diff --git a/packages/framework/fluid-framework/api-report/fluid-framework.legacy.alpha.api.md b/packages/framework/fluid-framework/api-report/fluid-framework.legacy.alpha.api.md index e8d7c1aad0d1..934ce06a7264 100644 --- a/packages/framework/fluid-framework/api-report/fluid-framework.legacy.alpha.api.md +++ b/packages/framework/fluid-framework/api-report/fluid-framework.legacy.alpha.api.md @@ -17,11 +17,22 @@ type ApplyKind = { [FieldKind.Identifier]: T; }[Kind]; +// @public +export type ApplyKindAssignment = [Kind] extends [ +FieldKind.Required +] ? T : [Kind] extends [FieldKind.Optional] ? T | undefined : never; + // @public type ApplyKindInput = [ Kind ] extends [FieldKind.Required] ? T : [Kind] extends [FieldKind.Optional] ? T | undefined : [Kind] extends [FieldKind.Identifier] ? DefaultsAreOptional extends true ? T | undefined : T : never; +// @public +export type AssignableTreeFieldFromImplicitField> = [TSchema] extends [FieldSchema] ? ApplyKindAssignment["readWrite"], Kind> : [TSchema] extends [ImplicitAllowedTypes] ? GetTypes["readWrite"] : never; + +// @public +export type AssignableTreeFieldFromImplicitFieldUnsafe> = TSchema extends FieldSchemaUnsafe ? ApplyKindAssignment["readWrite"], Kind> : GetTypesUnsafe["readWrite"]; + // @public export enum AttachState { Attached = "Attached", @@ -70,10 +81,40 @@ export interface ContainerSchema { readonly initialObjects: Record; } +// @public +export type CustomizedSchemaTyping = TSchema & { + [CustomizedTyping]: TCustom; +}; + +// @public +export const CustomizedTyping: unique symbol; + +// @public +export type CustomizedTyping = typeof CustomizedTyping; + +// @public @sealed +export interface CustomTypes { + readonly input: unknown; + readonly output: TreeLeafValue | TreeNode; + readonly readWrite: TreeLeafValue | TreeNode; +} + +// @public +export type DefaultInsertableTreeNodeFromImplicitAllowedTypes = [TSchema] extends [TreeNodeSchema] ? InsertableTypedNode : [TSchema] extends [AllowedTypes] ? InsertableTreeNodeFromAllowedTypes : never; + +// @public +export type DefaultInsertableTreeNodeFromImplicitAllowedTypesUnsafe> = [TSchema] extends [TreeNodeSchemaUnsafe] ? InsertableTypedNodeUnsafe : [TSchema] extends [AllowedTypesUnsafe] ? InsertableTreeNodeFromAllowedTypesUnsafe : never; + // @public @sealed interface DefaultProvider extends ErasedType<"@fluidframework/tree.FieldProvider"> { } +// @public +export type DefaultTreeNodeFromImplicitAllowedTypes = TSchema extends TreeNodeSchema ? NodeFromSchema : TSchema extends AllowedTypes ? NodeFromSchema> : unknown; + +// @public +export type DefaultTreeNodeFromImplicitAllowedTypesUnsafe> = TSchema extends TreeNodeSchemaUnsafe ? NodeFromSchemaUnsafe : TSchema extends AllowedTypesUnsafe ? NodeFromSchemaUnsafe> : unknown; + // @alpha (undocumented) export type DeserializeCallback = (properties: PropertySet) => void; @@ -149,6 +190,16 @@ export type FluidObject = { // @public export type FluidObjectProviderKeys = string extends TProp ? never : number extends TProp ? never : TProp extends keyof Required[TProp] ? Required[TProp] extends Required[TProp]>[TProp] ? TProp : never : never; +// @public +export type GetTypes = [TSchema] extends [ +CustomizedSchemaTyping +] ? TCustom : StrictTypes; + +// @public +export type GetTypesUnsafe> = [ +TSchema +] extends [CustomizedSchemaTyping] ? TCustom : StrictTypesUnsafe; + // @alpha export interface IBranchOrigin { id: string; @@ -531,7 +582,7 @@ export type InsertableObjectFromSchemaRecordUnsafe> = [TSchema] extends [FieldSchema] ? ApplyKindInput, Kind, true> : [TSchema] extends [ImplicitAllowedTypes] ? InsertableTreeNodeFromImplicitAllowedTypes : never; +export type InsertableTreeFieldFromImplicitField] ? TSchemaInput : SchemaUnionToIntersection> = [TSchema] extends [FieldSchema] ? ApplyKindInput, Kind, true> : [TSchema] extends [ImplicitAllowedTypes] ? InsertableTreeNodeFromImplicitAllowedTypes : never; // @public export type InsertableTreeFieldFromImplicitFieldUnsafe, TSchema = UnionToIntersection> = [TSchema] extends [FieldSchemaUnsafe] ? ApplyKindInput, Kind, true> : [TSchema] extends [ImplicitAllowedTypes] ? InsertableTreeNodeFromImplicitAllowedTypesUnsafe : never; @@ -549,12 +600,10 @@ LazyItem, ] ? InsertableTypedNodeUnsafe | InsertableTreeNodeFromAllowedTypesUnsafe : never; // @public -export type InsertableTreeNodeFromImplicitAllowedTypes = [ -TSchema -] extends [TreeNodeSchema] ? InsertableTypedNode : [TSchema] extends [AllowedTypes] ? InsertableTreeNodeFromAllowedTypes : never; +export type InsertableTreeNodeFromImplicitAllowedTypes = GetTypes["input"]; // @public -export type InsertableTreeNodeFromImplicitAllowedTypesUnsafe> = [TSchema] extends [TreeNodeSchemaUnsafe] ? InsertableTypedNodeUnsafe : [TSchema] extends [AllowedTypesUnsafe] ? InsertableTreeNodeFromAllowedTypesUnsafe : never; +export type InsertableTreeNodeFromImplicitAllowedTypesUnsafe> = GetTypesUnsafe["input"]; // @public export type InsertableTypedNode> = (T extends TreeNodeSchema ? NodeBuilderData : never) | (T extends TreeNodeSchema ? Unhydrated ? never : NodeFromSchema> : never); @@ -929,12 +978,30 @@ export interface NodeSchemaOptions { // @public type ObjectFromSchemaRecord> = { - -readonly [Property in keyof T]: Property extends string ? TreeFieldFromImplicitField : unknown; + -readonly [Property in keyof T as [ + AssignableTreeFieldFromImplicitField + ] extends [never | undefined] ? never : Property]: AssignableTreeFieldFromImplicitField; +} & { + readonly [Property in keyof T]: TreeFieldFromImplicitField; }; // @public type ObjectFromSchemaRecordUnsafe>> = { - -readonly [Property in keyof T]: TreeFieldFromImplicitFieldUnsafe; + -readonly [Property in keyof T as [T[Property]] extends [ + CustomizedSchemaTyping + ] ? never : Property]: AssignableTreeFieldFromImplicitFieldUnsafe; +} & { + readonly [Property in keyof T as [T[Property]] extends [ + CustomizedSchemaTyping + ] ? Property : never]: TreeFieldFromImplicitFieldUnsafe; }; // @public @@ -1081,6 +1148,11 @@ export const schemaStatics: { readonly requiredRecursive: (t: T_3, props?: Omit) => FieldSchemaUnsafe; }; +// @public +export type SchemaUnionToIntersection = [T] extends [ +CustomizedSchemaTyping +] ? T : UnionToIntersection; + // @public type ScopedSchemaName = TScope extends undefined ? `${TName}` : `${TScope}.${TName}`; @@ -1170,6 +1242,26 @@ export const SharedTree: SharedObjectKind; export { Side } +// @public @sealed +export interface StrictTypes, TOutput extends TreeNode | TreeLeafValue = DefaultTreeNodeFromImplicitAllowedTypes> { + // (undocumented) + input: TInput; + // (undocumented) + output: TOutput; + // (undocumented) + readWrite: TInput extends never ? never : TOutput; +} + +// @public +export interface StrictTypesUnsafe, TInput = DefaultInsertableTreeNodeFromImplicitAllowedTypesUnsafe, TOutput = DefaultTreeNodeFromImplicitAllowedTypesUnsafe> { + // (undocumented) + input: TInput; + // (undocumented) + output: TOutput; + // (undocumented) + readWrite: TOutput; +} + // @public export interface Tagged { // (undocumented) @@ -1280,10 +1372,10 @@ export interface TreeNodeApi { } // @public -export type TreeNodeFromImplicitAllowedTypes = TSchema extends TreeNodeSchema ? NodeFromSchema : TSchema extends AllowedTypes ? NodeFromSchema> : unknown; +export type TreeNodeFromImplicitAllowedTypes = GetTypes["output"]; // @public -type TreeNodeFromImplicitAllowedTypesUnsafe> = TSchema extends TreeNodeSchemaUnsafe ? NodeFromSchemaUnsafe : TSchema extends AllowedTypesUnsafe ? NodeFromSchemaUnsafe> : unknown; +type TreeNodeFromImplicitAllowedTypesUnsafe> = GetTypesUnsafe["output"]; // @public @sealed export type TreeNodeSchema = (TNode extends TreeNode ? TreeNodeSchemaClass : never) | TreeNodeSchemaNonClass; diff --git a/packages/framework/fluid-framework/api-report/fluid-framework.legacy.public.api.md b/packages/framework/fluid-framework/api-report/fluid-framework.legacy.public.api.md index 32ffa5a2ac1b..eb93cfcf17dc 100644 --- a/packages/framework/fluid-framework/api-report/fluid-framework.legacy.public.api.md +++ b/packages/framework/fluid-framework/api-report/fluid-framework.legacy.public.api.md @@ -17,11 +17,22 @@ type ApplyKind = { [FieldKind.Identifier]: T; }[Kind]; +// @public +export type ApplyKindAssignment = [Kind] extends [ +FieldKind.Required +] ? T : [Kind] extends [FieldKind.Optional] ? T | undefined : never; + // @public type ApplyKindInput = [ Kind ] extends [FieldKind.Required] ? T : [Kind] extends [FieldKind.Optional] ? T | undefined : [Kind] extends [FieldKind.Identifier] ? DefaultsAreOptional extends true ? T | undefined : T : never; +// @public +export type AssignableTreeFieldFromImplicitField> = [TSchema] extends [FieldSchema] ? ApplyKindAssignment["readWrite"], Kind> : [TSchema] extends [ImplicitAllowedTypes] ? GetTypes["readWrite"] : never; + +// @public +export type AssignableTreeFieldFromImplicitFieldUnsafe> = TSchema extends FieldSchemaUnsafe ? ApplyKindAssignment["readWrite"], Kind> : GetTypesUnsafe["readWrite"]; + // @public export enum AttachState { Attached = "Attached", @@ -70,10 +81,40 @@ export interface ContainerSchema { readonly initialObjects: Record; } +// @public +export type CustomizedSchemaTyping = TSchema & { + [CustomizedTyping]: TCustom; +}; + +// @public +export const CustomizedTyping: unique symbol; + +// @public +export type CustomizedTyping = typeof CustomizedTyping; + +// @public @sealed +export interface CustomTypes { + readonly input: unknown; + readonly output: TreeLeafValue | TreeNode; + readonly readWrite: TreeLeafValue | TreeNode; +} + +// @public +export type DefaultInsertableTreeNodeFromImplicitAllowedTypes = [TSchema] extends [TreeNodeSchema] ? InsertableTypedNode : [TSchema] extends [AllowedTypes] ? InsertableTreeNodeFromAllowedTypes : never; + +// @public +export type DefaultInsertableTreeNodeFromImplicitAllowedTypesUnsafe> = [TSchema] extends [TreeNodeSchemaUnsafe] ? InsertableTypedNodeUnsafe : [TSchema] extends [AllowedTypesUnsafe] ? InsertableTreeNodeFromAllowedTypesUnsafe : never; + // @public @sealed interface DefaultProvider extends ErasedType<"@fluidframework/tree.FieldProvider"> { } +// @public +export type DefaultTreeNodeFromImplicitAllowedTypes = TSchema extends TreeNodeSchema ? NodeFromSchema : TSchema extends AllowedTypes ? NodeFromSchema> : unknown; + +// @public +export type DefaultTreeNodeFromImplicitAllowedTypesUnsafe> = TSchema extends TreeNodeSchemaUnsafe ? NodeFromSchemaUnsafe : TSchema extends AllowedTypesUnsafe ? NodeFromSchemaUnsafe> : unknown; + // @public @sealed export abstract class ErasedType { static [Symbol.hasInstance](value: never): value is never; @@ -146,6 +187,16 @@ export type FluidObject = { // @public export type FluidObjectProviderKeys = string extends TProp ? never : number extends TProp ? never : TProp extends keyof Required[TProp] ? Required[TProp] extends Required[TProp]>[TProp] ? TProp : never : never; +// @public +export type GetTypes = [TSchema] extends [ +CustomizedSchemaTyping +] ? TCustom : StrictTypes; + +// @public +export type GetTypesUnsafe> = [ +TSchema +] extends [CustomizedSchemaTyping] ? TCustom : StrictTypesUnsafe; + // @public export interface IConnection { readonly id: string; @@ -459,7 +510,7 @@ export type InsertableObjectFromSchemaRecordUnsafe> = [TSchema] extends [FieldSchema] ? ApplyKindInput, Kind, true> : [TSchema] extends [ImplicitAllowedTypes] ? InsertableTreeNodeFromImplicitAllowedTypes : never; +export type InsertableTreeFieldFromImplicitField] ? TSchemaInput : SchemaUnionToIntersection> = [TSchema] extends [FieldSchema] ? ApplyKindInput, Kind, true> : [TSchema] extends [ImplicitAllowedTypes] ? InsertableTreeNodeFromImplicitAllowedTypes : never; // @public export type InsertableTreeFieldFromImplicitFieldUnsafe, TSchema = UnionToIntersection> = [TSchema] extends [FieldSchemaUnsafe] ? ApplyKindInput, Kind, true> : [TSchema] extends [ImplicitAllowedTypes] ? InsertableTreeNodeFromImplicitAllowedTypesUnsafe : never; @@ -477,12 +528,10 @@ LazyItem, ] ? InsertableTypedNodeUnsafe | InsertableTreeNodeFromAllowedTypesUnsafe : never; // @public -export type InsertableTreeNodeFromImplicitAllowedTypes = [ -TSchema -] extends [TreeNodeSchema] ? InsertableTypedNode : [TSchema] extends [AllowedTypes] ? InsertableTreeNodeFromAllowedTypes : never; +export type InsertableTreeNodeFromImplicitAllowedTypes = GetTypes["input"]; // @public -export type InsertableTreeNodeFromImplicitAllowedTypesUnsafe> = [TSchema] extends [TreeNodeSchemaUnsafe] ? InsertableTypedNodeUnsafe : [TSchema] extends [AllowedTypesUnsafe] ? InsertableTreeNodeFromAllowedTypesUnsafe : never; +export type InsertableTreeNodeFromImplicitAllowedTypesUnsafe> = GetTypesUnsafe["input"]; // @public export type InsertableTypedNode> = (T extends TreeNodeSchema ? NodeBuilderData : never) | (T extends TreeNodeSchema ? Unhydrated ? never : NodeFromSchema> : never); @@ -663,12 +712,30 @@ export interface NodeSchemaOptions { // @public type ObjectFromSchemaRecord> = { - -readonly [Property in keyof T]: Property extends string ? TreeFieldFromImplicitField : unknown; + -readonly [Property in keyof T as [ + AssignableTreeFieldFromImplicitField + ] extends [never | undefined] ? never : Property]: AssignableTreeFieldFromImplicitField; +} & { + readonly [Property in keyof T]: TreeFieldFromImplicitField; }; // @public type ObjectFromSchemaRecordUnsafe>> = { - -readonly [Property in keyof T]: TreeFieldFromImplicitFieldUnsafe; + -readonly [Property in keyof T as [T[Property]] extends [ + CustomizedSchemaTyping + ] ? never : Property]: AssignableTreeFieldFromImplicitFieldUnsafe; +} & { + readonly [Property in keyof T as [T[Property]] extends [ + CustomizedSchemaTyping + ] ? Property : never]: TreeFieldFromImplicitFieldUnsafe; }; // @public @@ -815,6 +882,11 @@ export const schemaStatics: { readonly requiredRecursive: (t: T_3, props?: Omit) => FieldSchemaUnsafe; }; +// @public +export type SchemaUnionToIntersection = [T] extends [ +CustomizedSchemaTyping +] ? T : UnionToIntersection; + // @public type ScopedSchemaName = TScope extends undefined ? `${TName}` : `${TScope}.${TName}`; @@ -830,6 +902,26 @@ export const SharedTree: SharedObjectKind; export { Side } +// @public @sealed +export interface StrictTypes, TOutput extends TreeNode | TreeLeafValue = DefaultTreeNodeFromImplicitAllowedTypes> { + // (undocumented) + input: TInput; + // (undocumented) + output: TOutput; + // (undocumented) + readWrite: TInput extends never ? never : TOutput; +} + +// @public +export interface StrictTypesUnsafe, TInput = DefaultInsertableTreeNodeFromImplicitAllowedTypesUnsafe, TOutput = DefaultTreeNodeFromImplicitAllowedTypesUnsafe> { + // (undocumented) + input: TInput; + // (undocumented) + output: TOutput; + // (undocumented) + readWrite: TOutput; +} + // @public export interface Tagged { // (undocumented) @@ -940,10 +1032,10 @@ export interface TreeNodeApi { } // @public -export type TreeNodeFromImplicitAllowedTypes = TSchema extends TreeNodeSchema ? NodeFromSchema : TSchema extends AllowedTypes ? NodeFromSchema> : unknown; +export type TreeNodeFromImplicitAllowedTypes = GetTypes["output"]; // @public -type TreeNodeFromImplicitAllowedTypesUnsafe> = TSchema extends TreeNodeSchemaUnsafe ? NodeFromSchemaUnsafe : TSchema extends AllowedTypesUnsafe ? NodeFromSchemaUnsafe> : unknown; +type TreeNodeFromImplicitAllowedTypesUnsafe> = GetTypesUnsafe["output"]; // @public @sealed export type TreeNodeSchema = (TNode extends TreeNode ? TreeNodeSchemaClass : never) | TreeNodeSchemaNonClass; diff --git a/packages/framework/fluid-framework/api-report/fluid-framework.public.api.md b/packages/framework/fluid-framework/api-report/fluid-framework.public.api.md index c3c3e20415ab..7eb32b8260a3 100644 --- a/packages/framework/fluid-framework/api-report/fluid-framework.public.api.md +++ b/packages/framework/fluid-framework/api-report/fluid-framework.public.api.md @@ -17,11 +17,22 @@ type ApplyKind = { [FieldKind.Identifier]: T; }[Kind]; +// @public +export type ApplyKindAssignment = [Kind] extends [ +FieldKind.Required +] ? T : [Kind] extends [FieldKind.Optional] ? T | undefined : never; + // @public type ApplyKindInput = [ Kind ] extends [FieldKind.Required] ? T : [Kind] extends [FieldKind.Optional] ? T | undefined : [Kind] extends [FieldKind.Identifier] ? DefaultsAreOptional extends true ? T | undefined : T : never; +// @public +export type AssignableTreeFieldFromImplicitField> = [TSchema] extends [FieldSchema] ? ApplyKindAssignment["readWrite"], Kind> : [TSchema] extends [ImplicitAllowedTypes] ? GetTypes["readWrite"] : never; + +// @public +export type AssignableTreeFieldFromImplicitFieldUnsafe> = TSchema extends FieldSchemaUnsafe ? ApplyKindAssignment["readWrite"], Kind> : GetTypesUnsafe["readWrite"]; + // @public export enum AttachState { Attached = "Attached", @@ -70,10 +81,40 @@ export interface ContainerSchema { readonly initialObjects: Record; } +// @public +export type CustomizedSchemaTyping = TSchema & { + [CustomizedTyping]: TCustom; +}; + +// @public +export const CustomizedTyping: unique symbol; + +// @public +export type CustomizedTyping = typeof CustomizedTyping; + +// @public @sealed +export interface CustomTypes { + readonly input: unknown; + readonly output: TreeLeafValue | TreeNode; + readonly readWrite: TreeLeafValue | TreeNode; +} + +// @public +export type DefaultInsertableTreeNodeFromImplicitAllowedTypes = [TSchema] extends [TreeNodeSchema] ? InsertableTypedNode : [TSchema] extends [AllowedTypes] ? InsertableTreeNodeFromAllowedTypes : never; + +// @public +export type DefaultInsertableTreeNodeFromImplicitAllowedTypesUnsafe> = [TSchema] extends [TreeNodeSchemaUnsafe] ? InsertableTypedNodeUnsafe : [TSchema] extends [AllowedTypesUnsafe] ? InsertableTreeNodeFromAllowedTypesUnsafe : never; + // @public @sealed interface DefaultProvider extends ErasedType<"@fluidframework/tree.FieldProvider"> { } +// @public +export type DefaultTreeNodeFromImplicitAllowedTypes = TSchema extends TreeNodeSchema ? NodeFromSchema : TSchema extends AllowedTypes ? NodeFromSchema> : unknown; + +// @public +export type DefaultTreeNodeFromImplicitAllowedTypesUnsafe> = TSchema extends TreeNodeSchemaUnsafe ? NodeFromSchemaUnsafe : TSchema extends AllowedTypesUnsafe ? NodeFromSchemaUnsafe> : unknown; + // @public @sealed export abstract class ErasedType { static [Symbol.hasInstance](value: never): value is never; @@ -146,6 +187,16 @@ export type FluidObject = { // @public export type FluidObjectProviderKeys = string extends TProp ? never : number extends TProp ? never : TProp extends keyof Required[TProp] ? Required[TProp] extends Required[TProp]>[TProp] ? TProp : never : never; +// @public +export type GetTypes = [TSchema] extends [ +CustomizedSchemaTyping +] ? TCustom : StrictTypes; + +// @public +export type GetTypesUnsafe> = [ +TSchema +] extends [CustomizedSchemaTyping] ? TCustom : StrictTypesUnsafe; + // @public export interface IConnection { readonly id: string; @@ -431,7 +482,7 @@ export type InsertableObjectFromSchemaRecordUnsafe> = [TSchema] extends [FieldSchema] ? ApplyKindInput, Kind, true> : [TSchema] extends [ImplicitAllowedTypes] ? InsertableTreeNodeFromImplicitAllowedTypes : never; +export type InsertableTreeFieldFromImplicitField] ? TSchemaInput : SchemaUnionToIntersection> = [TSchema] extends [FieldSchema] ? ApplyKindInput, Kind, true> : [TSchema] extends [ImplicitAllowedTypes] ? InsertableTreeNodeFromImplicitAllowedTypes : never; // @public export type InsertableTreeFieldFromImplicitFieldUnsafe, TSchema = UnionToIntersection> = [TSchema] extends [FieldSchemaUnsafe] ? ApplyKindInput, Kind, true> : [TSchema] extends [ImplicitAllowedTypes] ? InsertableTreeNodeFromImplicitAllowedTypesUnsafe : never; @@ -449,12 +500,10 @@ LazyItem, ] ? InsertableTypedNodeUnsafe | InsertableTreeNodeFromAllowedTypesUnsafe : never; // @public -export type InsertableTreeNodeFromImplicitAllowedTypes = [ -TSchema -] extends [TreeNodeSchema] ? InsertableTypedNode : [TSchema] extends [AllowedTypes] ? InsertableTreeNodeFromAllowedTypes : never; +export type InsertableTreeNodeFromImplicitAllowedTypes = GetTypes["input"]; // @public -export type InsertableTreeNodeFromImplicitAllowedTypesUnsafe> = [TSchema] extends [TreeNodeSchemaUnsafe] ? InsertableTypedNodeUnsafe : [TSchema] extends [AllowedTypesUnsafe] ? InsertableTreeNodeFromAllowedTypesUnsafe : never; +export type InsertableTreeNodeFromImplicitAllowedTypesUnsafe> = GetTypesUnsafe["input"]; // @public export type InsertableTypedNode> = (T extends TreeNodeSchema ? NodeBuilderData : never) | (T extends TreeNodeSchema ? Unhydrated ? never : NodeFromSchema> : never); @@ -627,12 +676,30 @@ export interface NodeSchemaOptions { // @public type ObjectFromSchemaRecord> = { - -readonly [Property in keyof T]: Property extends string ? TreeFieldFromImplicitField : unknown; + -readonly [Property in keyof T as [ + AssignableTreeFieldFromImplicitField + ] extends [never | undefined] ? never : Property]: AssignableTreeFieldFromImplicitField; +} & { + readonly [Property in keyof T]: TreeFieldFromImplicitField; }; // @public type ObjectFromSchemaRecordUnsafe>> = { - -readonly [Property in keyof T]: TreeFieldFromImplicitFieldUnsafe; + -readonly [Property in keyof T as [T[Property]] extends [ + CustomizedSchemaTyping + ] ? never : Property]: AssignableTreeFieldFromImplicitFieldUnsafe; +} & { + readonly [Property in keyof T as [T[Property]] extends [ + CustomizedSchemaTyping + ] ? Property : never]: TreeFieldFromImplicitFieldUnsafe; }; // @public @@ -779,6 +846,11 @@ export const schemaStatics: { readonly requiredRecursive: (t: T_3, props?: Omit) => FieldSchemaUnsafe; }; +// @public +export type SchemaUnionToIntersection = [T] extends [ +CustomizedSchemaTyping +] ? T : UnionToIntersection; + // @public type ScopedSchemaName = TScope extends undefined ? `${TName}` : `${TScope}.${TName}`; @@ -790,6 +862,26 @@ export interface SharedObjectKind extends ErasedTyp // @public export const SharedTree: SharedObjectKind; +// @public @sealed +export interface StrictTypes, TOutput extends TreeNode | TreeLeafValue = DefaultTreeNodeFromImplicitAllowedTypes> { + // (undocumented) + input: TInput; + // (undocumented) + output: TOutput; + // (undocumented) + readWrite: TInput extends never ? never : TOutput; +} + +// @public +export interface StrictTypesUnsafe, TInput = DefaultInsertableTreeNodeFromImplicitAllowedTypesUnsafe, TOutput = DefaultTreeNodeFromImplicitAllowedTypesUnsafe> { + // (undocumented) + input: TInput; + // (undocumented) + output: TOutput; + // (undocumented) + readWrite: TOutput; +} + // @public export interface Tagged { // (undocumented) @@ -900,10 +992,10 @@ export interface TreeNodeApi { } // @public -export type TreeNodeFromImplicitAllowedTypes = TSchema extends TreeNodeSchema ? NodeFromSchema : TSchema extends AllowedTypes ? NodeFromSchema> : unknown; +export type TreeNodeFromImplicitAllowedTypes = GetTypes["output"]; // @public -type TreeNodeFromImplicitAllowedTypesUnsafe> = TSchema extends TreeNodeSchemaUnsafe ? NodeFromSchemaUnsafe : TSchema extends AllowedTypesUnsafe ? NodeFromSchemaUnsafe> : unknown; +type TreeNodeFromImplicitAllowedTypesUnsafe> = GetTypesUnsafe["output"]; // @public @sealed export type TreeNodeSchema = (TNode extends TreeNode ? TreeNodeSchemaClass : never) | TreeNodeSchemaNonClass;