Haxeで型パラメータに構造的部分型を指定した時の挙動

執筆時のバージョン情報: Haxe 3.4.6

Haxe/JSでCosmos DBクライアント(npm documentdb)のexternを書いているのだが、Haxeでexternを書くたびにたまにハマることがある(ハマるたびにググってる)ので、メモを残しとく。

DocumentClient#createDocument() というAPIは、JSONid がrequired、ttl がoptional、あとは任意をフィールドを指定できる(指定した値がCosmos DBに保存される)インターフェースになっている。

これを何も考えずにexternに落とすと(というか、TypeScriptの.d.tsを下手に書き直すと)こんな感じになる。
※説明用に簡略化しているため、実際のnpmのインターフェースとは異なっているので注意。

@:jsRequire("documentdb", "DocumentClient")
extern class DocumentClient {
    function createDocument(link: String, body: Document): Void;
}

typedef Document = {
    var id: String;
    @:optional var ttl: Int;
    /* id以外のフィールドは動的設定可能 */
}

これをこんな感じで使おうとするとコンパイルが通らない。

// var client: DocumentClient = ...;
// var link = "...";

client.createDocument(link, {
    id: "xxxx",
    name: "hoge"
});

こんな感じのコンパイルエラーが出る。 name なんていうフィールドが余計についとるよと怒られる。

{ name : String, id : String } has extra field name

で、 Haxeのマニュアル に従ってexternを書き直すとこんな感じになる。

createDocument()の型パラメータとして <TBody: Document> を指定している。TBodyはDocument型で定義されたフィールドを最低限持ってれば、他に余計なフィールドを持っててもいいよという定義になる。

@:jsRequire("documentdb", "DocumentClient")
extern class DocumentClient {
    function createDocument<TBody: Document>(link: String, body: TBody): Void;
}

typedef Document = {
    var id: String;
    @:optional var ttl: Int;
    /* id以外のフィールドは動的設定可能 */
}

で、これでめでたしめでたしかと思いきや、コンパイルが通らない。

client.createDocument(link, {
    id: "xxxx",
    name: "hoge"
});
Constraint check failure for createDocument.TBody
{ name : String, id : String } should be js.npm.documentdb.Document
{ name : String, id : String } should be { ?ttl : Null<Int>, id : String }
{ name : String, id : String } has no field ttl

ttl フィールドがねーぞと怒られる。@:optionalだから許してくれてもいい気がするんだがー…。

仕方ないので、利用側のコードでこういう感じに書いて回避する。

client.createDocument(link, {
    id: "xxxx",
    ttl: js.Lib.undefined,  //nullだと期待通りに動作しないライブラリがある
    name: "hoge"
});

もしくは、次のようにDocumentをカスケーディングした型を定義して、型が自明になるようなコードにする。ここでは変数に型を明示しているが、関数を作って引数の型として指定するようなコードでも当然良い。

typedef User = {>Document,
    var name: String;
}
var user: User = {
    id: "xxxx",
    name: "hoge"
}

client.createDocument(link, user);