microsoft/FluidFramework client_v2.91.0
microsoft/FluidFramework
Captured source
source ↗Fluid Framework v2.91.0 (minor)
Repository: microsoft/FluidFramework
Tag: client_v2.91.0
Published: 2026-03-18T06:30:19Z
Prerelease: no
Release notes:
Contents
- [✨ New Features](#user-content--new-features)
- [Adds withDefault API to allow defining default values for required and optional fields (#26502)](#user-content-adds-withdefault-api-to-allow-defining-default-values-for-required-and-optional-fields-26502)
✨ New Features
Adds withDefault API to allow defining default values for required and optional fields (#26502)
The withDefault API is now available on SchemaFactoryAlpha. It allows you to specify default values for fields, making them optional in constructors even when the field is marked as required in the schema. This provides a better developer experience by reducing boilerplate when creating objects.
The withDefault API wraps a field schema and defines a default value to use when the field is not provided during construction. The default value must be of an allowed type of the field. You can provide defaults in two ways:
- A value: When a value is provided directly, the data is copied for each use to ensure independence between instances
- A generator function: A function that is called each time to produce a fresh value
Defaults are evaluated eagerly during node construction.
Required fields with defaults
import { SchemaFactoryAlpha, TreeAlpha } from "@fluidframework/tree/alpha";
const sf = new SchemaFactoryAlpha("example");
class Person extends sf.objectAlpha("Person", {
name: sf.required(sf.string),
age: sf.withDefault(sf.required(sf.number), -1),
role: sf.withDefault(sf.required(sf.string), "guest"),
}) {}
// Before: all fields were required
// const person = new Person({ name: "Alice", age: -1, role: "guest" });
// After: fields with defaults are optional
const person = new Person({ name: "Alice" });
// person.age === -1
// person.role === "guest"
// You can still provide values to override the defaults
const admin = new Person({ name: "Bob", age: 30, role: "admin" });Optional fields with custom defaults
Optional fields (sf.optional) already default to undefined, but withDefault allows you to specify a different default value:
class Config extends sf.object("Config", {
timeout: sf.withDefault(sf.optional(sf.number), 5000),
retries: sf.withDefault(sf.optional(sf.number), 3),
}) {}
// All fields are optional, using custom defaults when not provided
const config = new Config({});
// config.timeout === 5000
// config.retries === 3
const customConfig = new Config({ timeout: 10000 });
// customConfig.timeout === 10000
// customConfig.retries === 3Value defaults vs function defaults
When you provide a value directly, the data is copied for each use, ensuring each instance is independent:
class Metadata extends sf.object("Metadata", {
tags: sf.array(sf.string),
version: sf.number,
}) {}
class Article extends sf.object("Article", {
title: sf.required(sf.string),
// a node is provided directly, it is copied for each use
metadata: sf.withDefault(
sf.optional(Metadata),
new Metadata({ tags: [], version: 1 }),
),
// also works with arrays
authors: sf.withDefault(sf.optional(sf.array(sf.string)), []),
}) {}
const article1 = new Article({ title: "First" });
const article2 = new Article({ title: "Second" });
// each article gets its own independent copy
assert(article1.metadata !== article2.metadata);
article1.metadata.version = 2; // Doesn't affect article2
assert(article2.metadata.version === 1);Alternatively, you can use generator functions to explicitly create new instances:
class Article extends sf.object("Article", {
title: sf.required(sf.string),
// generators are called each time to create a new instance
metadata: sf.withDefault(
sf.optional(Metadata),
() => new Metadata({ tags: [], version: 1 }),
),
authors: sf.withDefault(sf.optional(sf.array(sf.string)), () => []),
}) {}Insertable object literals, arrays, and map objects can be used in place of node instances in both static defaults and generator functions:
class Article extends sf.object("Article", {
title: sf.required(sf.string),
// plain object literal instead of new Metadata(...)
metadata: sf.withDefault(sf.optional(Metadata), () => ({
tags: [],
version: 1,
})),
// plain array instead of new ArrayNode(...)
authors: sf.withDefault(sf.optional(sf.array(sf.string)), () => [
"anonymous",
]),
}) {}##### Dynamic defaults
Generator functions are called each time a new node is created, enabling dynamic defaults:
class Document extends sf.object("Document", {
id: sf.withDefault(sf.required(sf.string), () => crypto.randomUUID()),
title: sf.required(sf.string),
}) {}
const doc1 = new Document({ title: "First Document" });
const doc2 = new Document({ title: "Second Document" });
// doc1.id !== doc2.id (each gets a unique UUID)Generator functions also work with primitive types:
let counter = 0;
class GameState extends sf.object("GameState", {
playerId: sf.withDefault(sf.required(sf.string), () => `player-${counter++}`),
score: sf.withDefault(sf.required(sf.number), () =>
Math.floor(Math.random() * 100),
),
isActive: sf.withDefault(sf.required(sf.boolean), () => counter % 2 === 0),
}) {}Recursive types
withDefaultRecursive is available for use inside recursive schemas. Use objectRecursiveAlpha (rather than objectRecursive) when defining recursive schemas with defaults, as it correctly makes defaulted fields optional in the constructor for all field kinds including requiredRecursive. It works the same as withDefault but is necessary to avoid TypeScript's circular reference limitations.
class TreeNode extends sf.objectRecursiveAlpha("TreeNode", {
value: sf.number,
label: SchemaFactoryAlpha.withDefaultRecursive(
sf.optional(sf.string),
"untitled",
),
child: sf.optionalRecursive([() => TreeNode]),
}) {}
// `label` is optional in the constructor — the default is used when omitted
const leaf = new TreeNode({ value: 1 });
// leaf.label === "untitled"
const root = new TreeNode({ value: 0, label: "root", child: leaf });
// root.label === "root"
// root.child.label === "untitled"> Warning: Be careful about using the recursive type itself as a default value — this is likely to cause infinite recursion at...
Excerpt shown — open the source for the full document.