Most numeric IDs in Infinity Engine scripting are interchangeable at the byte level - a spell ID, a class ID, and a sound slot are all just number. The type system has nothing to say about whether 42 is a valid SpellID or a stray race ID someone fat-fingered. @bgforge/iets uses branded types to recover that distinction.
IE<T, B> helpertype Brand<B> = { __brand: B };
export type IE<T, B> = T & Brand<B>;
IE<number, "SpellID"> is structurally a number, but nominally distinct. The compiler treats SpellID and ClassID as incompatible even though both compile down to plain numbers.
The package ships pre-typed constants for every IDS file, so 99% of the time you never construct a branded value by hand:
import { ReallyForceSpell, Myself, WIZARD_SHIELD, WIZARD_FIREBALL } from "@bgforge/iets/bg2";
ReallyForceSpell(Myself, WIZARD_SHIELD); // OK
ReallyForceSpell(Myself, WIZARD_FIREBALL); // OK
ReallyForceSpell(Myself, 42); // type error: 42 is not a SpellID
When you need a custom value (a spell from a mod that isn't in the stock IDS, for example), cast it once at the boundary:
import type { SpellID } from "@bgforge/iets";
const MY_CUSTOM_SPELL = 9001 as SpellID;
ReallyForceSpell(Myself, MY_CUSTOM_SPELL); // OK
If your custom spell is reachable by resref rather than ID, use the *RES form of the action and pass a plain string:
ReallyForceSpellRES("WM_LIGHT", Myself); // OK - resref form, no cast needed
ResRef, SplRef, ItmRef, and friends are string & {} - they look branded but are actually equivalent to plain string. This is deliberate. Resrefs are almost always raw string literals ("SWORD01", "AR0602"), and there is no finite IDS to give you pre-typed constants. Branding would force a cast on every usage:
GiveItemCreate("SWORD01" as ItmRef, Player1, 0, 0, 0); // would be required
GiveItemCreate("SWORD01", Player1, 0, 0, 0); // what we actually want
The string & {} shape preserves literal-type inference (so "SWORD01" doesn't widen to plain string in inferred contexts) without adding ceremony.
Action brandEngine action functions return a branded Action interface:
export interface Action {
readonly __brand: "Action";
}
This lets ActionOverride enforce that its argument is an actual action call, not an arbitrary expression:
ActionOverride(Player1, MoveToObject(Player2)); // OK
ActionOverride(Player1, "MoveToObject"); // type error: not an Action
*ID get appended?Some IDS types use a *ID suffix (ClassID, GenderID, KitID), others use bare names (Align, EA, State). The suffix exists only when a same-named trigger or action function would clash:
Class() is a trigger -> the IDS type is ClassID.Align has no same-named function -> the IDS type is Align.If you're hunting for a type and don't find the bare name, try the *ID form.