Skip to content

Actors and Items

Johannes Loher edited this page Dec 5, 2021 · 3 revisions

Configuring the foundry-vtt-types to use the correct types for Actor and Item classes and their data models

Using foundry-vtt-types for your system development allows you to easily get proper type checking for your Actor and Item classes, including their data models for both the source data and any derived data.

To achieve this, declaration merging is used to inject your types to be used throughout the provided type definitions. This has the added benefit that the your types will automatically be used in a lot more places than before, which means less type assertions are needed on your end.

The process of configuring the correct types is exactly the same for both Item and Actor. It consists of 3 steps:

1. Configuring the Correct Types for _source

  • Start off by defining the data structure matching the schema defined in your template.json. Each (Actor / Item) type should have its own interface:
    interface <SomeDescriptiveName>DataSourceData {
      /* properties defined in the template.json for that particular type */
    }
  • Then for each type, you create another interface containing the type property set to the type of the (Actor / Item) and the data property set to the interface you just created:
    interface <SomeDescriptiveName>DataSource {
      type: '<the type>';
      data: <SomeDescriptiveName>DataSourceData;
    }
  • All of these interfaces are then combined into a union type:
    type <SomeDescriptiveNameForAllTypes>DataSource =
      | <SomeDescriptiveName>DataSource
      | /* other types... */
      | <SomeOtherDescriptiveName>DataSource;
  • This type is then injected to be used via declaration merging:
    declare global {
      interface SourceConfig {
        (Item / Data): <SomeDescriptiveNameForAllTypes>DataSource;
      }
    }

This setup automatic inference of the concrete _source.data by checking _source.type.

⚠️ These types should match exactly what you have defined in your template.json. It is important to keep them in sync because otherwise, the types don't reflect what is happening at runtime and you might run into errors.

Example for Item

interface ArmorDataSource {
  type: 'armor';
  data: ArmorDataSourceData;
}

interface WeaponDataSourceData {
  damagePerHit: number;
  attackSpeed: number;
}

interface WeaponDataSource {
  type: 'weapon';
  data: WeaponDataSourceData;
}

type MyItemDataSource = ArmorDataSource | WeaponDataSource;

declare global {
  interface SourceConfig {
    Item: MyItemDataSource;
  }
}

/* ... */

const item = await Item.create({name: 'Sword', type: 'weapon'});
if(item.data._source.type === 'weapon') {
  const attackSpeed = item.data._source.data.attackSpeed; // ok, TypeScript knows this exists :)
  /* ... */
}

2. Configuring the Correct Types for the prepared data

This works mostly the same as for _source but you include changes you make in your prepareData method in the types.

  • Start off by defining the data structure matching the data after prepareData has been called. Each (Actor / Item) type should have its own interface:
    interface <SomeDescriptiveName>DataPropertiesData {
      /* properties as they look after prepareData() */
    }
  • Then for each type, you create another interface containing the type property set to the type of the (Actor / Item) and the data property set to the interface you just created:
    interface <SomeDescriptiveName>DataProperties {
      type: '<the type>';
      data: <SomeDescriptiveName>DataPropertiesData;
    }
  • All of these interfaces are then combined into a union type:
    type <SomeDescriptiveNameForAllTypes>DataProperties =
      | <SomeDescriptiveName>DataProperties
      | /* other types... */
      | <SomeOtherDescriptiveName>DataProperties;
  • This type is then injected to be used via declaration merging:
    declare global {
      interface DataConfig {
        (Item / Data): <SomeDescriptiveNameForAllTypes>DataProperties;
      }
    }

This setup automatic inference of the concrete data.data by checking data.type.

⚠️ Again, these types should match actually happens in your prepareData method and it is important to keep them in sync because otherwise, the types don't reflect what is happening at runtime and you might run into errors.

Example for Item

(Assuming the above definitions for the _source)

interface ArmorDataPropertiesData extends ArmorDataSourceData {
  weight: number;
}

interface ArmorDataProperties {
  type: 'armor';
  data: ArmorDataPropertiesData;
}

interface WeaponDataPropertiesData extends WeaponDataSourceData {
  damage: number;
}

interface WeaponDataProperties {
  type: 'weapon';
  data: WeaponDataPropertiesData;
}

type MyItemDataProperties = ArmorDataProperties | WeaponDataProperties;

declare global {
  interface DataConfig {
    Item: MyItemDataProperties;
  }
}

/* ... */

const item = await Item.create({name: 'Sword', type: 'weapon'});
if(item.data.type === 'weapon') {
  const damage = item.data.data.damage; // ok, TypeScript knows that even this derived data exists :)
  /* ... */
}

A complete example showcasing these 2 steps can also be seen in item.test-d.ts

3. Configuring a Custom Document Class

Simply follow the instructions laid out in Custom Document Class Configuration to configure your custom (Item / Actor) class to be used.

Example for Item

export class MyItem extends Item { /* ... */ }

declare global {
  interface DocumentClassConfig {
    Item: typeof MyItem;
  }
}
import { MyItem } from './item';

Hooks.once('init', () => {
  CONFIG.Item.documentClass = MyItem;
});