Creating custom types
Custom types give your arguments validation, autocompletion, and transformations. You can register custom types just like you would hooks, using the central Registry module.
Writing type definition files
When a type is contained in a ModuleScript, it shouldn't return a plain table. Instead, it must return a function that accepts the Registry as its sole argument. From there, you register the custom type dictionary using registry:RegisterType("typeName", typeTable).
While missing a client-side registration won't inherently break execution on the server, skipping it means autocomplete functionality and live client validation will not exist for that type. To ensure a seamless user experience, register custom types across both environments.
Here is the template for a custom type module:
local myCustomType = {
Transform = function(text)
-- Step 1: Receive the raw input string
return string.lower(text)
end,
Validate = function(transformedValue)
-- Step 2: Validates the output from Transform
local isValid = #transformedValue > 3
return isValid, "The value must be longer than 3 characters."
end,
Parse = function(transformedValue)
-- Step 3: Returns the final value to the command implementation
return { value = transformedValue }
end,
}
return function(registry)
registry:RegisterType("myCustomType", myCustomType)
end
The type execution pipeline
When a user executes a command, their argument inputs run sequentially through a distinct execution pipeline. If any step fails or yields an error, the pipeline halts immediately.
Transform()- Input: Raw user string, calling
Player. - Description: Receives raw string text and the calling player.
- Input: Raw user string, calling
Validate()- Input: Output of
Transform(). - Returns:
trueor(false, errorText). - Description: Validates the transformed input before proceeding.
- Input: Output of
Autocomplete()(Client-side)- Input: Output of
Transform(). - Description: Runs on the client to display dynamic dropdown suggestions.
- Input: Output of
Parse()- Input: Output of
Transform(). - Returns: Final Luau type.
- Description: Converts the string into the target Luau type.
- Input: Output of
Structure of a TypeDefinition
Important: Of all the lifecycle methods listed below,
Parseis the only strictly required method. All other methods (Transform,Validate,Autocomplete, etc.) are entirely optional and can be omitted if your type doesn't need them.
Your type definition dictionary can implement several unique lifecycle methods:
Transform
Transform = function(text: string, player: Player) -> any
An optional function that accepts the raw text argument input and the player who executed the command. Whatever value this returns will be automatically passed down to Validate, Autocomplete, and Parse. If omitted, the raw string text is passed along instead.
Validate
Validate = function(transformedValue: any) -> (boolean, string?)
Evaluates whether the value matches your requirements. It receives the output returned from your Transform function. If valid, it must return true. If invalid, it should return false followed by a user-facing string explaining the validation failure. If omitted, all input is assumed valid.
ValidateOnce
ValidateOnce = function(transformedValue: any) -> (boolean, string?)
Works identically to Validate, but only executes after the user presses Enter. Use this exclusively for slow or yielding network calls (such as checking GetUserIdFromNameAsync via Players). For normal operations, stick to standard Validate blocks.
Autocomplete
Autocomplete = function(transformedValue: any) -> ({ string }, { IsPartial: boolean? }?)
Populates Cmdr's dropdown menu as users type. It must return an array of strings representing match options. You can optionally return a second dictionary parameter containing configuration options (e.g., set { IsPartial = true } to prevent pressing Tab from immediately proceeding to the next command argument).
Parse
Parse = function(transformedValue: any) -> any
[REQUIRED] The final stage of the lifecycle pipeline before the value is marked complete. This transforms the processed text tokens into the final Luau type object (e.g., an Instance, a Color3, or a table) which will be fed directly into your final command implementation module.
Specialized type options
Beyond basic processing, TypeDefinition objects can include metadata fields that dramatically alter how Cmdr interacts with them.
Listable types (Listable)
Setting Listable = true instructs Cmdr that comma-separated values are allowed (e.g., kill player1,player2,player3).
When enabled, you do not need to update your parsing or validation architecture. Cmdr automatically splits the values by their commas and processes each individual segment through your Transform, Validate, and Parse pipeline independently.
- Requirement: Your
Parsefunction must return a table. The values returned by each segment'sParsestep are automatically flattened and merged into a single unique table before being passed to your command.
Default overrides (Default)
You can provide fallback string options if an argument is skipped or if a user passes a period (.) literal token.
Default = function(player: Player) -> string
The Default function must always return a string. This string value is inserted and treated exactly as raw user text before any parsing or Transform operations are evaluated.
Type helper functions
Cmdr provides a few standard factory functions within Util to quickly spin up custom types without writing complex validation loops.
Enum types (Util.MakeEnumType)
For basic collections of discrete string choices, use Util.MakeEnumType(name, choices). This automatically provides full fuzzy-matching autocomplete suggestions and ensures the parsed output explicitly matches a key within your options array.
return function(registry)
local customEnum = registry.Cmdr.Util.MakeEnumType("rarity", { "Common", "Rare", "Epic", "Legendary" })
registry:RegisterType("rarity", customEnum)
end
Sequence types (Util.MakeSequenceType)
For multi-value structural types like positions, vectors, or colors, use Util.MakeSequenceType. It splits a space- or comma-delimited string sequence and runs validation logic on each piece.
local vector2Type = registry.Cmdr.Util.MakeSequenceType({
Length = 2,
TransformEach = tonumber,
ValidateEach = function(value, index)
return value ~= nil, `Component {index} must be a valid number.`
end,
Constructor = Vector2.new
})