-
Notifications
You must be signed in to change notification settings - Fork 56
Actors and Items
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:
- 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 thedata
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
.
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.
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 :)
/* ... */
}
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
afterprepareData
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 thedata
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
.
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.
(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
Simply follow the instructions laid out in Custom Document Class Configuration to configure your custom (Item / Actor) class to be used.
export class MyItem extends Item { /* ... */ }
declare global {
interface DocumentClassConfig {
Item: typeof MyItem;
}
}
import { MyItem } from './item';
Hooks.once('init', () => {
CONFIG.Item.documentClass = MyItem;
});