Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
88 changes: 88 additions & 0 deletions .changeset/lemon-deer-walk.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,88 @@
---
"@fluidframework/tree-agent": minor
---

tree-agent: New type factory system for method and property bindings

Check warning on line 5 in .changeset/lemon-deer-walk.md

View workflow job for this annotation

GitHub Actions / vale

[vale] reported by reviewdog 🐶 [Microsoft.Terms] Prefer 'personal digital assistant' over 'agent'. Raw Output: {"message": "[Microsoft.Terms] Prefer 'personal digital assistant' over 'agent'.", "location": {"path": ".changeset/lemon-deer-walk.md", "range": {"start": {"line": 5, "column": 6}}}, "severity": "INFO"}
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
tree-agent: New type factory system for method and property bindings
Adds new type factory system for method and property bindings


The `@fluidframework/tree-agent` package now includes a custom type system (Type Factory) as an alternative to Zod for
defining method and property types. This new system is available in the `/alpha` entry point and provides a familiar
API for type definitions.

## Key features

- **Familiar API**: Use `tf.string()`, `tf.object()`, etc. - similar to Zod's syntax (where `tf` is aliased from
`typeFactory`)
- **Same API surface**: The existing `expose`, `exposeProperty`, and `buildFunc` methods work with both Zod and Type
Factory types

## Usage

Import from the alpha entry point to use Type Factory types:

```typescript
import { typeFactory as tf, buildFunc, exposeMethodsSymbol } from "@fluidframework/tree-agent/alpha";
import { SchemaFactory } from "@fluidframework/tree";

const sf = new SchemaFactory("myApp");

class TodoList extends sf.object("TodoList", {
items: sf.array(sf.string),
}) {
public addItem(item: string): void {
this.items.insertAtEnd(item);
}

public static [exposeMethodsSymbol](methods) {
methods.expose(
TodoList,
"addItem",
buildFunc({ returns: tf.void() }, ["item", tf.string()])
);
}
}
```

## Available types

All common types are supported:

- **Primitives**: `tf.string()`, `tf.number()`, `tf.boolean()`, `tf.void()`, `tf.undefined()`, `tf.null()`,
`tf.unknown()`
- **Collections**: `tf.array(elementType)`, `tf.object({ shape })`, `tf.map(keyType, valueType)`,
`tf.record(keyType, valueType)`, `tf.tuple([types])`
- **Utilities**: `tf.union([types])`, `tf.literal(value)`, `tf.optional(type)`, `tf.readonly(type)`
- **Schema references**: `tf.instanceOf(SchemaClass)`

## Migration from Zod

You can migrate gradually - both Zod and Type Factory types work in the same codebase:

**Before (Zod):**

```typescript
import { z } from "zod";
import { buildFunc, exposeMethodsSymbol } from "@fluidframework/tree-agent";

methods.expose(
MyClass,
"myMethod",
buildFunc({ returns: z.string() }, ["param", z.number()])
);
```

**After (Type Factory):**

```typescript
import { typeFactory as tf, buildFunc, exposeMethodsSymbol } from "@fluidframework/tree-agent/alpha";

methods.expose(
MyClass,
"myMethod",
buildFunc({ returns: tf.string() }, ["param", tf.number()])
);
```

## Note on type safety

The Type Factory type system does not currently provide compile-time type checking, though this may be added in the
future. For applications requiring strict compile-time validation, Zod types remain fully supported.
1 change: 1 addition & 0 deletions .vale/config/vocabularies/fluid/accept.txt
Original file line number Diff line number Diff line change
Expand Up @@ -15,3 +15,4 @@ Datastore
DDSes
accessor
accessors
Zod
183 changes: 163 additions & 20 deletions packages/framework/tree-agent/api-report/tree-agent.alpha.api.md
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,7 @@
```ts

// @alpha
export type Arg<T extends z.ZodTypeAny = z.ZodTypeAny> = readonly [name: string, type: T];
export type Arg<T extends z.ZodTypeAny | TypeFactoryType = z.ZodTypeAny | TypeFactoryType> = readonly [name: string, type: T];

// @alpha
export type ArgsTuple<T extends readonly Arg[]> = T extends readonly [infer Single extends Arg] ? [Single[1]] : T extends readonly [infer Head extends Arg, ...infer Tail extends readonly Arg[]] ? [Head[1], ...ArgsTuple<Tail>] : never;
Expand All @@ -17,7 +17,7 @@ export type AsynchronousEditor<TSchema extends ImplicitFieldSchema> = (tree: Vie
export type BindableSchema = TreeNodeSchema<string, NodeKind.Object> | TreeNodeSchema<string, NodeKind.Record> | TreeNodeSchema<string, NodeKind.Array> | TreeNodeSchema<string, NodeKind.Map>;

// @alpha
export function buildFunc<const Return extends z.ZodTypeAny, const Args extends readonly Arg[], const Rest extends z.ZodTypeAny | null = null>(def: {
export function buildFunc<const Return extends z.ZodTypeAny | TypeFactoryType, const Args extends readonly Arg[], const Rest extends z.ZodTypeAny | TypeFactoryType | null = null>(def: {
description?: string;
returns: Return;
rest?: Rest;
Expand Down Expand Up @@ -53,19 +53,23 @@ export type ExposableKeys<T> = {

// @alpha
export interface ExposedMethods {
// (undocumented)
expose<const K extends string & keyof MethodKeys<InstanceType<S>>, S extends BindableSchema & Ctor<Record<K, Infer<Z>>> & IExposedMethods, Z extends FunctionDef<any, any, any>>(schema: S, methodName: K, zodFunction: Z): void;
expose<const K extends string & keyof MethodKeys<InstanceType<S>>, S extends BindableSchema & Ctor<Record<K, InferZod<Z>>> & IExposedMethods, Z extends FunctionDef<readonly Arg<z.ZodTypeAny>[], z.ZodTypeAny, z.ZodTypeAny | null>>(schema: S, methodName: K, zodFunction: Z): void;
expose<const K extends string & keyof MethodKeys<InstanceType<S>>, S extends BindableSchema & Ctor & IExposedMethods, Z extends FunctionDef<readonly Arg<TypeFactoryType>[], TypeFactoryType, TypeFactoryType | null>>(schema: S, methodName: K, tfFunction: Z): void;
instanceOf<T extends TreeNodeSchemaClass>(schema: T): z.ZodType<InstanceType<T>, z.ZodTypeDef, InstanceType<T>>;
}

// @alpha
export interface ExposedProperties {
// (undocumented)
exposeProperty<S extends BindableSchema & Ctor, K extends string & ExposableKeys<InstanceType<S>>, TZ extends ZodTypeAny>(schema: S, name: K, def: {
schema: TZ;
description?: string;
} & ReadOnlyRequirement<InstanceType<S>, K> & TypeMatchOrError<InstanceType<S>[K], infer<TZ>>): void;
// (undocumented)
exposeProperty<S extends BindableSchema & Ctor, K extends string & ExposableKeys<InstanceType<S>>>(schema: S, name: K, def: {
schema: TypeFactoryType;
description?: string;
readOnly?: boolean;
}): void;
exposeProperty<S extends BindableSchema & Ctor, K extends string & ExposableKeys<InstanceType<S>>>(schema: S, name: K, tfType: TypeFactoryType): void;
instanceOf<T extends TreeNodeSchemaClass>(schema: T): ZodType<InstanceType<T>, ZodTypeDef, InstanceType<T>>;
}

Expand All @@ -76,34 +80,46 @@ export const exposeMethodsSymbol: unique symbol;
export const exposePropertiesSymbol: unique symbol;

// @alpha
export interface FunctionDef<Args extends readonly Arg[], Return extends z.ZodTypeAny, Rest extends z.ZodTypeAny | null = null> {
// (undocumented)
export interface FunctionDef<Args extends readonly Arg[], Return extends z.ZodTypeAny | TypeFactoryType, Rest extends z.ZodTypeAny | TypeFactoryType | null = null> {
args: Args;
// (undocumented)
description?: string;
// (undocumented)
rest?: Rest;
// (undocumented)
returns: Return;
}

// @alpha
export interface IExposedMethods {
// (undocumented)
[exposeMethodsSymbol](methods: ExposedMethods): void;
}

// @alpha
export interface IExposedProperties {
// (undocumented)
[exposePropertiesSymbol]?(properties: ExposedProperties): void;
}

// @alpha
export type IfEquals<X, Y, A = true, B = false> = (<T>() => T extends X ? 1 : 2) extends <T>() => T extends Y ? 1 : 2 ? A : B;

// @alpha @deprecated
export type Infer<T> = T extends FunctionDef<readonly Arg[], infer Return, any> ? Return extends z.ZodTypeAny ? InferZod<T> : InferTypeFactory<T> : never;

// @alpha
export type InferArgsZod<Args extends readonly Arg<z.ZodTypeAny>[]> = Args extends readonly [
infer Head extends Arg<z.ZodTypeAny>,
...infer Tail extends readonly Arg<z.ZodTypeAny>[]
] ? [z.infer<Head[1]>, ...InferArgsZod<Tail>] : [];

// @alpha
export type InferTypeFactory<T> = T extends FunctionDef<readonly Arg[], infer Return, any> ? (...args: any[]) => any : never;

// @alpha
export type InferZod<T> = T extends FunctionDef<infer Args extends readonly Arg<z.ZodTypeAny>[], infer Return extends z.ZodTypeAny, any> ? (...args: InferArgsZod<Args>) => z.infer<Return> : never;

// @alpha
export const instanceOfsTypeFactory: WeakMap<TypeFactoryInstanceOf, ObjectNodeSchema<string, RestrictiveStringRecord<ImplicitFieldSchema_2>, boolean, unknown>>;

// @alpha
export type Infer<T> = T extends FunctionDef<infer Args, infer Return, infer Rest> ? z.infer<z.ZodFunction<z.ZodTuple<ArgsTuple<Args>, Rest>, Return>> : never;
export function isTypeFactoryType(value: unknown): value is TypeFactoryType;

// @alpha
export const llmDefault: unique symbol;
Expand All @@ -120,15 +136,15 @@ export type MethodKeys<T> = {

// @alpha
export class PropertyDef {
constructor(name: string, description: string | undefined, schema: ZodTypeAny, readOnly: boolean);
// (undocumented)
constructor(
name: string,
description: string | undefined,
schema: ZodTypeAny | TypeFactoryType,
readOnly: boolean);
readonly description: string | undefined;
// (undocumented)
readonly name: string;
// (undocumented)
readonly readOnly: boolean;
// (undocumented)
readonly schema: ZodTypeAny;
readonly schema: ZodTypeAny | TypeFactoryType;
}

// @alpha
Expand Down Expand Up @@ -183,6 +199,133 @@ export type SynchronousEditor<TSchema extends ImplicitFieldSchema> = (tree: View
// @alpha
export type TreeView<TRoot extends ImplicitFieldSchema> = Pick<TreeViewAlpha<TRoot>, "root" | "fork" | "merge" | "rebaseOnto" | "schema" | "events"> & TreeBranchAlpha;

// @alpha
export const typeFactory: {
string(): TypeFactoryString;
number(): TypeFactoryNumber;
boolean(): TypeFactoryBoolean;
void(): TypeFactoryVoid;
undefined(): TypeFactoryUndefined;
null(): TypeFactoryNull;
unknown(): TypeFactoryUnknown;
array(element: TypeFactoryType): TypeFactoryArray;
object(shape: Record<string, TypeFactoryType>): TypeFactoryObject;
record(keyType: TypeFactoryType, valueType: TypeFactoryType): TypeFactoryRecord;
map(keyType: TypeFactoryType, valueType: TypeFactoryType): TypeFactoryMap;
tuple(items: readonly TypeFactoryType[], rest?: TypeFactoryType): TypeFactoryTuple;
union(options: readonly TypeFactoryType[]): TypeFactoryUnion;
literal(value: string | number | boolean): TypeFactoryLiteral;
optional(innerType: TypeFactoryType): TypeFactoryOptional;
readonly(innerType: TypeFactoryType): TypeFactoryReadonly;
instanceOf<T extends TreeNodeSchemaClass_2>(schema: T): TypeFactoryInstanceOf;
};

// @alpha
export interface TypeFactoryArray extends TypeFactoryType {
readonly element: TypeFactoryType;
readonly _kind: "array";
}

// @alpha
export interface TypeFactoryBoolean extends TypeFactoryType {
readonly _kind: "boolean";
}

// @alpha
export interface TypeFactoryInstanceOf extends TypeFactoryType {
readonly _kind: "instanceof";
readonly schema: ObjectNodeSchema;
}

// @alpha
export interface TypeFactoryLiteral extends TypeFactoryType {
readonly _kind: "literal";
readonly value: string | number | boolean;
}

// @alpha
export interface TypeFactoryMap extends TypeFactoryType {
readonly keyType: TypeFactoryType;
readonly _kind: "map";
readonly valueType: TypeFactoryType;
}

// @alpha
export interface TypeFactoryNull extends TypeFactoryType {
readonly _kind: "null";
}

// @alpha
export interface TypeFactoryNumber extends TypeFactoryType {
readonly _kind: "number";
}

// @alpha
export interface TypeFactoryObject extends TypeFactoryType {
readonly _kind: "object";
readonly shape: Record<string, TypeFactoryType>;
}

// @alpha
export interface TypeFactoryOptional extends TypeFactoryType {
readonly innerType: TypeFactoryType;
readonly _kind: "optional";
}

// @alpha
export interface TypeFactoryReadonly extends TypeFactoryType {
readonly innerType: TypeFactoryType;
readonly _kind: "readonly";
}

// @alpha
export interface TypeFactoryRecord extends TypeFactoryType {
readonly keyType: TypeFactoryType;
readonly _kind: "record";
readonly valueType: TypeFactoryType;
}

// @alpha
export interface TypeFactoryString extends TypeFactoryType {
readonly _kind: "string";
}

// @alpha
export interface TypeFactoryTuple extends TypeFactoryType {
readonly items: readonly TypeFactoryType[];
readonly _kind: "tuple";
readonly rest?: TypeFactoryType;
}

// @alpha
export interface TypeFactoryType {
readonly _kind: TypeFactoryTypeKind;
}

// @alpha
export type TypeFactoryTypeKind = "string" | "number" | "boolean" | "void" | "undefined" | "null" | "unknown" | "array" | "object" | "record" | "map" | "tuple" | "union" | "literal" | "optional" | "readonly" | "instanceof";

// @alpha
export interface TypeFactoryUndefined extends TypeFactoryType {
readonly _kind: "undefined";
}

// @alpha
export interface TypeFactoryUnion extends TypeFactoryType {
readonly _kind: "union";
readonly options: readonly TypeFactoryType[];
}

// @alpha
export interface TypeFactoryUnknown extends TypeFactoryType {
readonly _kind: "unknown";
}

// @alpha
export interface TypeFactoryVoid extends TypeFactoryType {
readonly _kind: "void";
}

// @alpha
export type TypeMatchOrError<Expected, Received> = [Received] extends [Expected] ? unknown : {
__error__: "Zod schema value type does not match the property's declared type";
Expand Down
31 changes: 31 additions & 0 deletions packages/framework/tree-agent/src/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -37,6 +37,9 @@ export {
type BindableSchema,
type Ctor,
type Infer,
type InferZod,
type InferArgsZod,
type InferTypeFactory,
type IExposedMethods,
} from "./methodBinding.js";
export type {
Expand All @@ -50,3 +53,31 @@ export type {
TypeMatchOrError,
IfEquals,
} from "./propertyBinding.js";

export {
typeFactory,
isTypeFactoryType,
instanceOfsTypeFactory,
} from "./treeAgentTypes.js";

export type {
TypeFactoryType,
TypeFactoryTypeKind,
TypeFactoryString,
TypeFactoryNumber,
TypeFactoryBoolean,
TypeFactoryVoid,
TypeFactoryUndefined,
TypeFactoryNull,
TypeFactoryUnknown,
TypeFactoryArray,
TypeFactoryObject,
TypeFactoryRecord,
TypeFactoryMap,
TypeFactoryTuple,
TypeFactoryUnion,
TypeFactoryLiteral,
TypeFactoryOptional,
TypeFactoryReadonly,
TypeFactoryInstanceOf,
} from "./treeAgentTypes.js";
Loading
Loading