Summary
First, check tests/std/trait/.check.luau on the main branch to see the current trait conventions.
When a class implements one or more traits using trait.impl, its prototype must be typed as __index = Class :: {} to avoid an ambiguous-call error caused by intersection types. As a side effect, any method defined directly on the class — one not required by any trait — becomes invisible to the type system on instances of that class.
Background
The trait system in src/std/trait/init.luau defines a trait through three pieces:
Requires<Self> — the shape of required methods,
Defaults — a table of default-method implementations,
For<Self> — Requires<Self> & Defaults.
A class that implements a trait declares its instance type as an intersection of its own shape with each For<Self>:
export type Box<T> = setmetatable<
{ read value: T },
typeof(BoxPrototype)
> & Display.For<Box<T>> & Copyable.For<Box<T>>
If BoxPrototype.__index is typed as the class table directly (__index = Box), each required method appears twice in the final intersection — once from the class table (the implementer redefines it) and once from Requires<Self>. Luau then treats them as overloaded function types and refuses to dispatch:
Calling function ... is ambiguous
The current workaround is to widen __index to {} at its declaration site:
local BoxPrototype = table.freeze({
__index = Box :: {} -- erase the class table's type to avoid the duplicate
})
This silences the ambiguity, but only because Box's own methods are no longer reachable through typeof(BoxPrototype).
The problem
The workaround holds as long as the class defines only methods that are required by some trait it implements. The moment the class adds a method of its own, the type system cannot see it on instances:
function Box.unpack<T>(self: Box<T>): T
return self.value
end
local b = Box.new(42)
b:unpack() -- type error: unknown property `unpack` on `Box<number>`
The method exists at runtime — the underlying table has it — but Box<T>'s type no longer mentions unpack, because typeof(BoxPrototype)'s __index was widened to {}.
Reproduction
Write a class that implements any trait(s) (e.g. simple Display trait) yourself and implement its own method and try to call it.
Expected behavior
A class should be able to expose its own methods alongside the methods required by the traits it implements, and those methods should be visible on the instance type.
Summary
First, check tests/std/trait/.check.luau on the main branch to see the current trait conventions.
When a class implements one or more traits using
trait.impl, its prototype must be typed as__index = Class :: {}to avoid an ambiguous-call error caused by intersection types. As a side effect, any method defined directly on the class — one not required by any trait — becomes invisible to the type system on instances of that class.Background
The trait system in
src/std/trait/init.luaudefines a trait through three pieces:Requires<Self>— the shape of required methods,Defaults— a table of default-method implementations,For<Self>—Requires<Self> & Defaults.A class that implements a trait declares its instance type as an intersection of its own shape with each
For<Self>:If
BoxPrototype.__indexis typed as the class table directly (__index = Box), each required method appears twice in the final intersection — once from the class table (the implementer redefines it) and once fromRequires<Self>. Luau then treats them as overloaded function types and refuses to dispatch:The current workaround is to widen
__indexto{}at its declaration site:This silences the ambiguity, but only because
Box's own methods are no longer reachable throughtypeof(BoxPrototype).The problem
The workaround holds as long as the class defines only methods that are required by some trait it implements. The moment the class adds a method of its own, the type system cannot see it on instances:
The method exists at runtime — the underlying table has it — but
Box<T>'s type no longer mentionsunpack, becausetypeof(BoxPrototype)'s__indexwas widened to{}.Reproduction
Write a class that implements any trait(s) (e.g. simple
Displaytrait) yourself and implement its own method and try to call it.Expected behavior
A class should be able to expose its own methods alongside the methods required by the traits it implements, and those methods should be visible on the instance type.