JS Extern trouble: how to make a callback function out of a typedef?

Hello :slight_smile: . I’ve used the famous dts2hx (ahem @haxiomic ) to generate externs from the Node library simple git. But I ran into a world of trouble trying to call functions there.

For starters, I’m trying to get the status of a git repo using simpleGit.status(). And I actually managed to do it IF I make a SMALL change in the transpiled JS. And this tells me I’m very close to the result, but oh, yet so far :frowning:

It all seems to revolve around an interface that defines a callback function via a generic type. This is the interface in TS:

/**
 * The node-style callback to a task accepts either two arguments with the first as a null
 * and the second as the data, or just one argument which is an error.
 */
export interface SimpleGitTaskCallback<T = string> {
    (err: null, data: T): void;
    (err: Error): void;
}

And this is the same in Haxe:

typedef SimpleGitTaskCallback<T> = {
	@:overload(function(err:js.lib.Error):Void {})
	@:selfCall
	function call(err:Dynamic, data:T):Void;
};

Now, I’m not ExEx (extern expert) but it’s strange that there’s that selfcall there. From @haxiomic’s earlier help I remember that the selfcall is used to call a class. But this is a typedef. And it’s not even a typedef for a class, but for a function. I’m probably missing something here, but anyway, I did what I could with that Haxe code. Which is this:

I defined a function like this:

	public static function statusResultCallback(err:Dynamic, data:StatusResult):Void {
		console.dir(err);
		console.dir(data);
	}

And then I assigned it like this:

var funcStatusResultCallback:SimpleGitTaskCallback<StatusResult> = {call: statusResultCallback};

But… when calling

simpleGit.status(null, funcStatusResultCallback);

I get nothing.

The involved lines of the transpiled code are this:

Test.statusResultCallback = function(err,data) {
	console.dir(err);
	console.dir(data);
var funcStatusResultCallback = { call : Test.statusResultCallback};
simpleGit.status(null,funcStatusResultCallback);
};

And this doesn’t work.

BUT

If I directly call Test.statusResultCallback, it works!

simpleGit.status(null,Test.statusResultCallback);

So… I suspect I’m missing something around how I could make that typedef into a function argument. I tried all manners of things, such as changing the return type of the function, or what is contained in the declaration of the function object, but I get compile errors this way and that. For the sake of your sanity, I’ll not go into details about all the weird hacks I’ve been doing :smiley:

To ease the effort of the kind soul that can take a look at this, I pushed this to GitHub. Just npm install it, run npx dts2hx simple-git and you’re good to go. You don’t need to create a Git repo to test the code :wink: it will use its own repo :smiley: (oh, the amusement of giving a Git repo to help solve a Git library problem using its own Git repo :smiley: ; should’ve posted this on GitHub, would’ve been even better)

And guess what, I’ll probably turn this into a “tutorial-repo”, a-la my live reload boilerplate repository. I’ve been struggling with quite a few externs situations and probably some knowledge sharing with the community wouldn’t hurt.

I see what’s happening here, and the confusion is understandable – this is a limitation in the way @:selfCall works. @:selfCall is a bit of a hack just for externs – the ‘call’ field doesn’t actually exist in js and you can’t set it like { call: fn }, but I agree that’s not obvious. @:selfCall only works when you’re calling the field – so it translates T.call() into T().

In this case in TypeScript we have an interface that has no fields and two call signatures – well really it’s not being used as an interface at all then, it’s just a function type so it’s a little strange way to write it semantically but in TS you can do this. What dts2hx could do here is detect we have an interface with no fields and instead replace this with

typedef SimpleGitTaskCallback<T> = ts.AnyOf2<(err: js.lib.Err) -> Void, (err: Dynamic, data: T) -> Void>

This could be a simple-ish change so I’ll add it in soon and let you know – I’ve opened an issue to track: Interface -> Function types if interface has no fields · Issue #73 · haxiomic/dts2hx · GitHub
It’s gets more complicated if another type implements this interface but I have pattern that resolves that

In the mean time a quick work around would be to do this instead

var funcStatusResultCallback:SimpleGitTaskCallback<StatusResult> = untyped statusResultCallback;
1 Like

You’re all manners of awesome man :slight_smile: Thank you! This has been weighing in the back of my mind for about a week now. Had to deal with other stuff before I could muster the energy to write a proper question around it.

Even though I knew of untyped, I never used it. Using it means learning it. Great idea and application (even though sure, untyped is not recommended).

TypeScript seems to be almost TOO permissive in the amount of jugglery you can do with types. It gets almost ridiculous at times. I can see why some people shun it. Personally, I like it 'cause I always liked quirky languages and clowns… but I like Haxe more :).

Alright, give this a shot again in 0.15.0 (just published), it now does the type conversion I mention so you shouldn’t need the untyped* anymore

1 Like