Hyperion

Typed input

Reading options one by one with getString() gets tedious, and you still have to check every value yourself. Typed input lets you describe the options with a schema instead. Hyperion validates them and hands your handler typed values, or the list of problems.

A command with input

import { ChatInputCommandBuilder } from "@discordjs/builders";
import { defineCommand } from "@hyperion-js/core";
import { MessageFlags } from "discord.js";
import * as v from "valibot";

export default defineCommand({
    builder: new ChatInputCommandBuilder()
        .setName("echo")
        .setDescription("Repeat a message.")
        .addStringOptions(o =>
            o.setName("text").setDescription("What to say").setRequired(true),
        )
        .addIntegerOptions(o =>
            o.setName("times").setDescription("How many times"),
        ),
    input: {
        schema: v.object({
            text: v.pipe(
                v.string(),
                v.trim(),
                v.minLength(1, "Say something!"),
            ),
            times: v.optional(v.number(), 1),
        }),
    },
    run: async ctx => {
        const [issues, value] = ctx.input;
        if (issues) {
            await ctx.interaction.reply({
                content: issues.map(issue => issue.message).join("\n"),
                flags: MessageFlags.Ephemeral,
            });
            return;
        }

        await ctx.interaction.reply(value.text.repeat(value.times));
    },
});

The builder still decides what Discord shows, and the schema decides what's valid and fills in defaults. Any Standard Schema library works. input takes:

OptionWhat it's for
schemaChecks the options and gives them their types.
resolveOptional. Turns the checked values into what your handler needs. See resolving the values.
deferOptional. Defers before validating. See deferring.

Everything is typed from the schema, with no annotations. To share input between handlers, define it once with defineInput() and pass that instead.

ctx.input is either [null, value] or [issues, undefined]. Hyperion never replies to validation problems for you, so you choose the wording. Return after handling issues and TypeScript knows value is safe.

What the schema receives

The schema gets one key per option of the command, or of the subcommand that ran:

OptionThe schema gets
Left out by the userundefined; use v.optional() for a default
String, integer, number, booleanThe value
User, role, channel, mentionableThe ID
AttachmentThe attachment

Resolving the values

Often you want the thing an option points to, not the raw value. Add resolve to turn validated values into whatever your handler needs:

input: {
    schema: v.object({ id: v.string() }),
    defer: true,
    resolve: async ({ id }) => ({ todo: await todos.find(id) }),
},

Now ctx.input holds { todo } instead of { id }. The resolver only runs when validation passes, and whatever it returns becomes the handler's input, so a missing todo can simply be null. Its second argument is the handler's context.

Deferring

Lookups can be slow, so Hyperion can defer before validating:

input: { schema, defer: true }, // a private reply, for any handler
input: { schema, defer: "update" }, // the message, for select menus and modals

Either way, finish with editReply(). See responding.

The order things happen

guards → defer (optional) → validate → resolve → your handler

Guards run first and never see ctx.input, so a rejected user never triggers your lookups. If the schema or resolver throws (rather than reporting issues), the user gets the generic error message.

Subcommands, modals, select menus, and context menus

  • Subcommands take input the same way.
  • Modal forms do too; see modals.
  • Select menus too; see select menus.
  • Context-menu commands have no options, so they don't take input.

On this page