rune

docs · authoring

Types.

Rune ships TypeScript types not by maintaining a hand-written API package but by reflecting your live classpath at runtime and emitting .d.ts files. If a plugin is on the server, your IDE knows about its classes. If it isn't, it doesn't. The types match reality.

What gets generated, and when

On every server start, after the Paper plugin enables and before any script loads, the host walks the live classpath and writes three things:

  • plugins/Rune/types/bukkit.d.ts — every public class under org.bukkit.*, io.papermc.paper.*, and net.kyori.adventure.*.
  • plugins/Rune/types/events.d.ts — every subclass of org.bukkit.event.Event that the reflector finds, including those contributed by other plugins, plus the Events enum that backs @EventHandler(Events.X).
  • plugins/Rune/types/<alias>.d.ts — one per plugin declared in your rune.jsonc's plugins map, emitted from the plugin's package.

The files are committed to disk so your editor can pick them up without running the server. They're regenerated on every start so they stay accurate as you add or remove plugins.

Wiring them into tsconfig

The scaffolded tsconfig.json already includes the right paths. If you're hand-rolling one, point TypeScript at the types directory and let it discover everything.

{
  "compilerOptions": {
    "target":      "esnext",
    "module":      "esnext",
    "moduleResolution": "bundler",
    "strict":      true,
    "types":       ["@rune/sdk"],
    "experimentalDecorators": true,
    "emitDecoratorMetadata":   true
  },
  "include": [
    "**/*.ts",
    "../../types/**/*.d.ts"
  ]
}

The experimentalDecorators flag is needed for @Listener / @Command / friends to type-check. Rune uses TC39 stage-3 decorators at runtime; the legacy TS-decorator flag is just for type-checking compatibility today and will go away once TS 5.9 stabilizes stage-3 emit.

Author-owned types.d.ts

If your Rune exposes its own globals (ward exposes ward.registerTransaction(...) on the global scope), declare them in a types.d.ts at your project root. TypeScript will merge them with the host-generated declarations.

// ward/types.d.ts
declare global {
  const ward: WardApi;
}

interface WardApi {
  registerTransaction<A extends TransactionArgs>(
    handler: string,
    impl: TransactionHandler<A>,
  ): void;
  unregisterTransaction(handler: string): void;
}

export {};   // make this a module so 'declare global' works

What's typed and what isn't

The reflector tries hard to be accurate but has limits.

Typed

  • Public constructors, methods, and fields on every reflected class.
  • Enum values as union types — autocomplete on bukkit.Material lists every block and item.
  • Generic parameters with their declared bounds (so List<Player> from Java surfaces as JavaList<Player> in TS).

Not typed

  • Anonymous and synthetic classes (anything Java's Class#isSynthetic() reports).
  • Methods on package-private classes — they exist at runtime if you can reach them, but autocomplete won't suggest them.
  • Reflective access via rune.callStatic with a string FQN. By design — you're telling the host you know what you're doing.

Adding types for a plugin not in rune.jsonc

If you want intellisense for a plugin you're not formally declaring (perhaps for one-off interop), write the types yourself in types.d.ts using the global proxy. The runtime call path is unaffected.

declare const someplugin: {
  Api: {
    getInstance(): {
      getThing(id: string): SomeThing | null;
    };
  };
};

interface SomeThing {
  getName(): string;
}