Problem with recursive generic type expansion

I’m trying to create a type-safe fluent/chaining API in Luau where each chained call accumulates new types using intersections.

Example:

export type ContextProperties<A = {[string]: ActionInstance?}> = {
	Context: InputContext,
	Name: string,
	Priority: number,
	Enabled: boolean,
	Sink: boolean,
	Actions: A,
}

export type InputInstance<A = {[string]: ActionInstance?}> = {
	CreateContext: (Name: string) -> InputInstance<A>,

	CreateAction: <N>(
		self: InputInstance<A>,
		Name: N & string,
		Cooldown: number?
	) -> InputInstance<A & {[N]: ActionInstance}>,

	Destroy: (self: any) -> ()
} & ContextProperties<A>

Usage:

local ctx =
	CreateContext()
		:CreateAction("Jump")
		:CreateAction("Dash")

The goal is for Luau to infer:

ctx.Actions.Jump
ctx.Actions.Dash

correctly after chaining.

However, Luau throws type expansion / recursive type errors when using:

A & {[N]: Action}

inside the recursive generic alias.

I discovered that the issue disappears if I remove the intersection accumulation and simply return:

InputInstance<A>

which makes me think the problem is related to recursive generic intersection expansion in fluent APIs.

I also noticed that moving the generic accumulation out of the type alias and into implementation casts (:: any) avoids the error.

Is this a current limitation of Luau’s type system, or is there a recommended pattern for implementing type-safe builder/fluent APIs with accumulating generics?

If you have any questions. Or you need full version of my type module. Feel free to ask!
Have a nice day:).

Your InputInstance type is subject to Recursive Type Restriction as it is used inside itself generically.

yea, i run into that sometimes. GoodSignal also has that problem too soooo idk if its a big deal but if typechecking works, then it works

oof, i have no idea else making the type like that :/…
Seems like roblox havent implemented the relax the recursive type restriction.

You should instead move the constructors into a separate structure type, this is pretty much the only way to do this until RTR is relaxed.