Skip to content

Type

luau
local Operator = require(game:GetService("ServerStorage").Operator)
local Type = Operator.Type
-- or
local Type = Operator.Type

Builds an argument type: something that turns one piece of typed text into a real value, and optionally offers autocomplete suggestions. One definition serves both sides. The client uses it for suggestions and inline validation, the server re-runs the same transform authoritatively at execution time.

Functions

Type.new

luau
Type.new<T>(name: string): Builder<T>
luau
return Type.new("itemId")
	:transform(function(raw, executor)
		local item = ItemConfig.Items[raw]
		if item == nil then
			return false, `"{raw}" is not an item`
		end
		return true, item
	end)
	:suggestions(function(query, executor)
		return ItemConfig.NamesMatching(query)
	end, { queryDependent = true })
	:build()

Type.runTransform

luau
Type.runTransform(argType: AnyArgType, raw: any, executor: Player?): (boolean, any)

Runs a type's transform with its timeout applied. A transform that raises, times out, or returns nothing at all is reported as a normal rejection with a message. It never escapes as an error.

luau
local ok, value = Type.runTransform(Types.Player, "@me", executor)
if not ok then
	warn(value)
end

Type.runSuggestions

luau
Type.runSuggestions(argType: AnyArgType, query: string, executor: Player?): (boolean, { string })

Runs a type's suggestions with its timeout applied.

Builder<T>

:transform

luau
:transform(fn: TransformFn): Builder<T>

Sets the transform. Return two values, never a wrapper object:

luau
return true, value      -- success
return false, "message" -- rejection, with a message shown to the player

A transform must be pure

It may read game state and it may yield, but it must not change anything: no writing data, no firing remotes, no mutating shared tables, no side effects of any kind.

The console runs your transform locally to show inline errors as the player types, and to keep the render path from ever blocking it resumes the transform in a coroutine and closes it the moment it yields. A transform that yields is abandoned partway through. If it had already mutated something, that mutation is left half-applied, on every keystroke.

The authoritative run happens on the server, which always runs the transform to completion. Put side effects in the command's run, where they belong.

:suggestions

luau
:suggestions(fn: SuggestionsFn, options: SuggestionOptions?): Builder<T>

May return a list directly or a promise-like value (anything with andThen). Requests are debounced and results cached.

:extends

luau
:extends(base: AnyArgType): Builder<T>

Runs the base transform first and feeds its result forward, so you never reimplement parsing. If the base rejects, the chain short-circuits and the base message is returned.

luau
local PositiveInteger = Type.new("positiveInteger")
	:extends(Types.Integer)
	:transform(function(value)
		if value <= 0 then
			return false, "expected a number above zero"
		end
		return true, value
	end)
	:build()

A type that defines no suggestions of its own inherits the base ones, along with the base caching options.

:timeout

luau
:timeout(seconds: number): Builder<T>

Overrides the transform timeout for this type. The default is 5 seconds.

:build

luau
:build(): ArgType<T>

Returns the finished type.

Types

ArgType<T>

luau
type ArgType<T> = {
	name: string,
	transform: TransformFn,
	suggestions: SuggestionsFn?,
	suggestionsQueryDependent: boolean,
	transformTimeout: number,
	suggestionTtl: number,
	suggestionDebounce: number,
	suggestionTimeout: number,
}

AnyArgType is ArgType<any>.

TransformFn

luau
type TransformFn = (value: any, executor: Player?) -> (boolean, any)

executor is the player running the command, or nil when there is not one.

SuggestionsFn

luau
type SuggestionsFn = (query: string, executor: Player?) -> any

SuggestionOptions

luau
type SuggestionOptions = {
	ttl: number?,
	debounce: number?,
	timeout: number?,
	queryDependent: boolean?,
}
OptionDefaultMeaning
ttl10seconds a cached suggestion list stays fresh
debounce0.15seconds of quiet before a fetch is issued
timeout5seconds before a suggestion fetch is abandoned
queryDependentfalsewhether the result depends on what has been typed

queryDependent is the one worth understanding. By default a type ignores query and returns its whole candidate list; the cache stores one entry per type and filters by prefix on the way out, so typing ten characters costs one fetch. Set it to true when the type narrows results itself, such as a DataStore lookup over thousands of rows, and the cache keys on the query and passes results through unfiltered.

Released under the MIT Licence.