ECMAScript
ECMAScript copied to clipboard
generate strong types for engine signals and connections
is there any interest in generating strong types for connections such as:
(this is probably a poor example, I'd need to dig in and make a PR to do it correctly)
declare module godot {
interface SceneTree implements MainLoop {
//overload 1 for network_peer_connected signal
function connect<T extends Object>(
signal: "network_peer_connected",
target: T,
method: Extract<T, Function> | (...args: Parameters<onNetworkPeerConnected>) => void, //could use e.g. MethodOf<T>,
binds: Tail<Parameters<onNetworkPeerConnected>>,
flags: number = 0,
): number;
// more overloads, one per signal for discriminating union
}
}
another place that stronger inference would generate useful tooling: although I would need to research if it's even possible.
function rpc_id<T extends {} = {}, MethodKey extends key of T, Method = T[]>(id: number, method: MethodKey, ...args: Parameters<Method>): any;
In godot signal just a string constant. That seems a lot of work to do but gains no difference in JavaScript.
I'm just personally a big fan of strong typing in typescript, and it offers better intellisense/tooling, such as having a type error if you pass arguments that don't correspond to the connected signal. I can look into it and offer a PR and better proof of concept
i.e.
this.get_tree().connect("network_peer_connected", (id: string, otherArg: number) => {}, "other");
that will be a type error because the id argument should be a number, and a string was passed to the otherArg when a number is required.
This is an interesting and useful suggestion. I was able to implement such a thing for C# using godot4-like signal declaration. For now, I am on the way to implementing this for our project (and it goes much much simpler). There is a code snippet of how this can work in typescript:
// This part should be defined somewhere in the ECMAScript
// The implementation of `emit` and `connect` is omitted to keep things simple
// In real Life each signal instance should keep owenr_id and name (which is set by
// ECMAscript when object instantiating) and bypass connection to singleton proxy Object
// ProxyObject well receive all Godot signals and additional argument bypassed
// by Signal instance - `callback_id` and then calls exact function.
// Godot signal are generated as properties which create instances of Signal
// on demand.
type VarArgs = readonly unknown[];
type Action<T extends VarArgs> = (...args: T) => void;
class Signal<T extends VarArgs> implements PromiseLike<T> {
object_id: Number = 0
name: String = ""
// showcase
callbacks:Action<T>[] = []
public emit(...args: T) {
// something from real world:
// godot.instance_from_id(this.instance_id).emit(this.name, ...args)
// just for showcase
for (const cb of this.callbacks) {
cb(...args)
}
}
public connect(cb: Action<T>) {
// real world:
// let obj = godot.instance_from_id(this.instance_id)
// let callback_id = Proxy.get_instance().register_callback(cb)
// obj.connect(this.name, Proxy.get_instance(), "handle_signal", [callback_id])
// just for showcase
this.callbacks.push(cb);
}
// this is required for awaiting directly for signals
public then<TResult1 = T>(onfulfilled?: ((value: T) => (PromiseLike<TResult1> | TResult1)) | undefined | null): PromiseLike<TResult1> {
return new Promise<TResult1>( (resolve, reject) => {
this.connect((...args: T) => {
if (typeof onfulfilled === 'function') {
resolve(onfulfilled(args))
} else {
// exception?
reject("Don't know how to complete without onfulfilled");
}
})
})
}
}
// the rest part is the usage example
class Obj /* extends godot.Object */ {
// @godot.typedsignal - for registering signal nad providing name and owner_id (or maybe generating property)
pressed: Signal<[]> = new Signal();
input: Signal<[name: String]> = new Signal();
selected: Signal<[name: String, age: Number]> = new Signal();
public async ready() {
console.log("in ready")
this.input.connect(this.handle_input) // <= everything typed as excpected
this.pressed.connect(() => console.log("pressed"))
this.input.connect((name) => console.log("input", name))
this.selected.connect((name, age) => console.log("selected", name, age))
// faking signals for showcase
setTimeout(() => this.pressed.emit(), 100)
setTimeout(() => this.input.emit("jhj"), 200);
setTimeout(() => this.selected.emit("vasia", 21), 300);
console.log("before handle")
await this.handle()
console.log("after handle")
}
public handle_input(name: String) {
console.log("Handling input with handle_input method", name)
}
public async handle(a: String = "" ) {
console.log("in handle")
await this.pressed
console.log("after pressed")
let [key] = await this.input; // key is str
console.log("after input", key)
let [name, age] = await this.selected; // name is str, age is num
console.log("after selected", name, age)
}
}
let n = new Obj()
n.ready()
/* Output:
[LOG]: "in ready"
[LOG]: "before handle"
[LOG]: "in handle"
[LOG]: "pressed"
[LOG]: "after pressed"
[LOG]: "Handling input with handle_input method", "jhj"
[LOG]: "input", "jhj"
[LOG]: "after input", "jhj"
[LOG]: "selected", "vasia", 21
[LOG]: "after selected", "vasia", 21
[LOG]: "after handle"
*/
This approach has some memory overhead but adding a lot of impact and stability. It is also fully compatible
with Godot API, e.g. I am able to emit a signal from the engine using old good emit_signal
and so on.
There is Playground Link