Default Field Values
The withDefault API is 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 in the constructor
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.objectAlpha("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 === 3
Value 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.objectAlpha("Metadata", {
tags: sf.array(sf.string),
version: sf.number,
}) {}
class Article extends sf.objectAlpha("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.objectAlpha("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.objectAlpha("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.objectAlpha("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.objectAlpha("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"
Be careful about using the recursive type itself as a default value — this is likely to cause infinite recursion at construction time, since creating the default value would trigger the same default again. Instead, use a primitive or a separate node type as the default:
const DefaultTag = sf.objectRecursiveAlpha("Tag", {
id: sf.number,
child: sf.optionalRecursive([() => TreeNode]),
});
class TreeNode extends sf.objectRecursiveAlpha("TreeNode", {
value: sf.number,
// ✅ Safe: the default uses a different recursive type, not TreeNode itself
tag: SchemaFactoryAlpha.withDefaultRecursive(
sf.optional(DefaultTag),
() => new DefaultTag({ id: 0 }),
),
child: sf.optionalRecursive([() => TreeNode]),
}) {}
The following definition for child would cause infinite recursion at construction time:
child: SchemaFactoryAlpha.withDefaultRecursive(
sf.optionalRecursive([() => TreeNode]),
() => new TreeNode({ value: 0 }),
);
The infinite recursion can be solved by passing in undefined explicitly, but it is
recommended to not use defaults in this case:
child: SchemaFactoryAlpha.withDefaultRecursive(
sf.optionalRecursive([() => TreeNode]),
() => new TreeNode({ value: 0, child: undefined }),
);
Type Safety
The default value (or the value returned by a generator function) must be of an allowed type for the field. TypeScript enforces this at compile time:
// ✅ Valid: number default for number field
sf.withDefault(sf.optional(sf.number), 8080);
// ✅ Valid: generator returns string for string field
sf.withDefault(sf.optional(sf.string), () => "localhost");
// ❌ TypeScript error: string default for number field
sf.withDefault(sf.optional(sf.number), "8080");
// ❌ TypeScript error: generator returns number for string field
sf.withDefault(sf.optional(sf.string), () => 8080);
See Also
- Schema Definition — Overview of defining schemas with
SchemaFactory - Setting Properties as Optional — Using
sf.optional()for optional fields - Recursive Schema — Defining recursive types with
objectRecursive() SchemaFactoryAlphaAPI — API reference forwithDefaultandwithDefaultRecursive- Schema Evolution — Safely updating schemas over time