eslint-plugin-minecraft-isvalid
v0.1.0
Published
Type-aware ESLint rules that enforce isValid() checks on @minecraft/server references after invalidation points.
Maintainers
Readme
eslint-plugin-minecraft-isvalid
Type-aware ESLint rules that enforce isValid checks on @minecraft/server
references after invalidation points — and flag the redundant checks you no
longer need.
What this protects you against
In the Bedrock Script API, almost everything you hold onto is a native
handle — Entity, Player, Block, Container, ContainerSlot,
components, ScreenDisplay, ScoreboardObjective, and more. These handles can
become invalid at any time: an entity unloads or dies, a block is broken or
its chunk unloads, a player disconnects. Once a handle is invalid, reading or
calling almost any member throws (InvalidEntityError, etc.).
The danger is that the handle looks fine in your code — the crash only happens at
runtime, usually a tick or more later, after an await or inside a deferred
callback where the world has moved on:
// 💥 Crashes if the player logged off during the await
async function teleportSoon(player: Player) {
await system.waitTicks(40);
player.teleport(target); // player may be invalid now
}
// 💥 Crashes if the entity unloaded before this callback runs
function blowUpLater(entity: Entity) {
system.runTimeout(() => {
entity.applyDamage(100); // entity may be invalid now
}, 100);
}These bugs are easy to write, hard to spot in review, and show up as
hard-to-reproduce production errors. require-is-valid-check catches them at
lint time and tells you exactly where a check is missing:
async function teleportSoon(player: Player) {
await system.waitTicks(40);
if (!player.isValid) return; // ✅ re-validate after the await
player.teleport(target);
}Once you have checked (or the API guarantees a live handle),
no-unnecessary-is-valid-check flags the leftover checks so they don't pile
up as noise.
The rules are type-aware: they use TypeScript type information — plus an
optional Valid* type-branding convention (see below) — to track exactly which
references are known-valid at each point, so they stay quiet on safe code and
only fire where a real invalidation boundary was crossed.
Rules
| Rule | Description | Recommended |
| ------------------------------------------------------------------------------ | --------------------------------------------------------------------- | ----------- |
| require-is-valid-check | Ensure references are validated before use after invalidation points. | error |
| no-unnecessary-is-valid-check | Warn when checking isValid on references already known to be valid. | warn |
Installation
npm install --save-dev eslint-plugin-minecraft-isvalidBoth rules require typed linting,
so you also need typescript and @typescript-eslint/parser.
Usage (flat config)
// eslint.config.mjs
import tseslint from 'typescript-eslint';
import minecraftIsValid from 'eslint-plugin-minecraft-isvalid';
export default tseslint.config({
files: ['src/**/*.ts'],
languageOptions: {
parser: tseslint.parser,
parserOptions: { projectService: true },
},
plugins: { 'minecraft-isvalid': minecraftIsValid },
rules: {
'minecraft-isvalid/require-is-valid-check': 'error',
'minecraft-isvalid/no-unnecessary-is-valid-check': 'warn',
},
});Or extend the bundled recommended config:
import minecraftIsValid from 'eslint-plugin-minecraft-isvalid';
export default [
// ...your type-aware language options...
minecraftIsValid.configs.recommended,
];The Valid* branding convention
The rules get most of their precision from a structural brand that marks a
reference as "already validated" — e.g. ValidPlayer = Player & { readonly __validMinecraftReferenceBrand?: true }.
APIs that always return live handles (world.getAllPlayers(), event payloads,
…) are typed to return the branded variant.
This package ships that augmentation. Activate it once anywhere in your project:
/// <reference types="eslint-plugin-minecraft-isvalid/valid-types" />You can still mix in your own project-specific @minecraft/server
augmentations; they merge with the shipped one.
Without the branding, the rules still work — they simply treat every tracked reference as "plain" and require a check before each use.
Use Valid* types in your APIs
The Valid* helpers are not just an internal implementation detail. They are one
of the best ways to make your own code communicate its safety contract.
If a function requires a handle that has already been checked, type the parameter as the branded version:
import type { ValidEntity, ValidPlayer } from '@minecraft/server';
function awardKillCredit(player: ValidPlayer, defeated: ValidEntity) {
// No extra isValid check needed here: the function contract says both handles
// must be live when this synchronous helper is called.
player.sendMessage(`Defeated ${defeated.typeId}`);
}Callers can satisfy that contract by checking isValid first:
import type { Entity, Player } from '@minecraft/server';
function maybeAwardKillCredit(player: Player, defeated: Entity) {
if (!player.isValid || !defeated.isValid) return;
awardKillCredit(player, defeated);
}This pattern is especially useful for shared helpers, form/menu openers, model
methods, event utilities, and any API where accepting a plain Player/Entity
would force every callee to repeat the same guard. Use the most specific branded
type that matches the value you expect: ValidPlayer, ValidEntity,
ValidBlock, ValidContainer, ValidContainerSlot, ValidEntityComponent,
and so on.
The brand means "known valid at this point in synchronous execution." If the function itself crosses an invalidation boundary, re-check before using the handle again:
async function rewardAfterDelay(player: ValidPlayer) {
await system.waitTicks(20);
if (!player.isValid) return; // required again after the await
player.sendMessage('Reward granted!');
}Options
See each rule's documentation for the full schema. require-is-valid-check
accepts:
trackedTypes— the@minecraft/servertype names to enforce. The defaults cover every exported handle that has anisValidmember, and matching is inheritance-aware (e.g.Entityalso coversPlayer;Componentcovers every concrete component).trackedModules— the modules tracked types must originate from (default:@minecraft/server).safeProperties— a per-type map of members that stay readable even on an invalidated handle. This is deliberately type-scoped: onlyEntity.idandEntity.typeIdare documented as safe by the API, so the default is{ "Entity": ["id", "typeId"] }(inherited byPlayer/SimulatedPlayer). A property liketypeIdis not assumed safe on other handles such asBlockorContainerSlot.
Development
npm install
npm run build # compile to dist/
npm test # run the type-aware rule tests
npm run lint # lint the plugin source
npm run typecheck # type-check without emittingSee AGENTS.md for the architecture and contribution workflow.
