Generic Extern Types
A runtime that returns a result from every file read wants scripts to know what each result holds. The result is an extern type: the host defines it and scripts only see its declaration. Because extern types and their methods can take generic parameters in Luwu, one Result<T> covers every call and the type checker knows what unwrap gives back.
Since the fork, Luau and Luwu have diverged here. Luau 0.742's parser expects with right after an extern type's name and reads no generic parameters on its methods.
Generic parameters
An embedder declares the type in a definitions file:
declare extern type Result<T>
function unwrap(self): T
function unwrap_or(self, default: T): T
function map<U>(self, transform: (T) -> U): Result<U>
end
declare function read_file(path: string): Result<string>Scripts get the type that each call fills in. Here map turns a Result<string> into a Result<number>:
const contents = read_file("config.toml")
const size = contents:map(function(text)
return #text
end)
const bytes: number = size:unwrap()
const label: string = size:unwrap_or(0)
print(bytes, label)main.luwu(7,23): TypeError: Expected 'label' to be 'string', but was given 'number'Generic methods
A method can declare generic parameters after its name, like map<U> above. They work like a generic function's: each call infers them from its arguments and they can appear in the return type alongside the extern type's parameters.
Defaults
Like the parameters of a generic type alias, a generic parameter can have a default. Since with is optional in Luwu and user code can declare extern types, this example runs as a plain module:
declare extern type Channel<Message = string>
function send(self, message: Message)
function receive(self): Message?
end
declare function open_channel<Message>(name: string): Channel<Message>
type Move = { x: number, y: number }
const chat: Channel = open_channel("chat")
const moves: Channel<Move> = open_channel("moves")
chat:send("gg")
moves:send({ x = 1, y = 2 })
moves:send("north")main.luwu(15,12): TypeError: Expected this to be
'Move: { x: number, y: number }'
but was given
'string'
'string' is unrelated to 'Move'.Narrowing with typeof
Generic parameters exist only for the type checker. At runtime typeof gives the extern type's name without its parameters, and checking it narrows a union to every member with that name. Checking a field then narrows to the right instantiation:
declare extern type Failure<Reason>
reason: Reason
end
declare function load(path: string): string | Failure<string> | Failure<{ code: number }>const save = load("save.dat")
if typeof(save) == "Failure" then
if typeof(save.reason) == "string" then
print(`load failed: {save.reason}`)
else
print(`load failed with code {save.reason.code}`)
end
else
print(`loaded {#save} bytes`)
endNo errors.Things to know
- Nothing at runtime records what
Tan extern type was used with. The host is trusted to return what its declaration says. - Inferred types include their parameters:
sizein the first example is aResult<number>.