Skip to content

Data models

The schema types itself

ts
import { BaseTypeDataModel, fields } from '@vttforge/core';

const defineCharacterSchema = () => {
  const f = fields();
  return {
    level: new f.NumberField({ required: true, nullable: false, initial: 1 }),
    health: new f.SchemaField({
      value: new f.NumberField({ required: true, nullable: false, initial: 10 }),
      max: new f.NumberField({ required: true, nullable: false, initial: 10 }),
    }),
  };
};

export class CharacterData extends BaseTypeDataModel(defineCharacterSchema) {
  declare armorClass: number;

  prepareDerivedData() {
    this.armorClass = 10 + this.level; // this.level is number
  }
}

type CharacterSystem = CharacterData['$inferData'];

You write the schema once. There is no second type declaration to keep in sync, and this.level inside prepareDerivedData is a number.

It has to be a function: fields() reads a Foundry global that does not exist when your module is first evaluated.

Declare your derived values

armorClass is not in the schema, so it is not on the type. Declaring it puts it on the type as a value that exists once prepareDerivedData has run.

In JavaScript there is no declare, and a plain class field would emit and reset the property to undefined after every data preparation. Put derived values in the schema instead, as a NumberField with initial: 0, and assign them in prepareDerivedData. The scaffold's JavaScript template does this for the ability modifiers.

Nullability defaults

Every field class picks its own defaults, and they disagree.

FieldWith no optionsWhy
NumberFieldnumber | null | undefinedoptional and nullable by default
StringFieldstring | undefinedoptional
BooleanFieldbooleanrequired, starts at false
HTMLFieldstringrequired, blank-friendly
ColorFieldColor | nullstarts at null
FilePathFieldstring | nullstarts at null
ArrayField / SetFieldnever absentrequired, builds its own empty value

So new f.NumberField() is not a number. Pass the options explicitly:

ts
new f.NumberField({ required: true, nullable: false, initial: 0 })

The inference reads the literal types of what you pass. An options object held in a variable widens nullable: false to boolean, and the field's own default applies again. Pin it with as const, or build the field in a small factory so the literals stay inline.

Fields that are not what they look like

FieldHolds
ColorFielda Color instance, not a string
SetFielda Set, not an array
ForeignDocumentFieldthe document; the model installs it as a getter
EmbeddedDataFieldthe model instance, with its derived data
TypedSchemaFielda union you can narrow on type

MIT licensed.