Haxeで型パラメータに構造的部分型を指定した時の挙動
執筆時のバージョン情報: Haxe 3.4.6
Haxe/JSでCosmos DBクライアント(npm documentdb)のexternを書いているのだが、Haxeでexternを書くたびにたまにハマることがある(ハマるたびにググってる)ので、メモを残しとく。
DocumentClient#createDocument() というAPIは、JSONで id
が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);