既存JavaScriptをTypeScriptとして修正する方法

JavaScriptはTypeScriptです。ほぼほぼ。.jsを.tsとして変更すれば動きます。というほど世の中甘くなくて、まあ、大抵は動きません。えー、なにそれ、欠陥品じゃないの?と思われるかもですが、いえ、結構単純な話です。例えばですが

var x = 0;
x = "hogehoge";

このコード、JavaScriptとしては正しいですが、TypeScriptとしては間違っていてコンパイル通りません。xがnumberとして推論されるので、"hogehoge"が代入できないからです。じゃあどうするの?というと、

var x: any = 0;
x = "hogehoge";

anyとして型定義してやればいいんですね。もしくは

var x = <any>0;
x = "hogehoge";

でもいいですが。<>はキャストみたいなものです。ちなみに、こういったことの実例はTypeScriptのソースをダウンロードしてきて、\src\harness\external\json2.ts に、json2.jsをtsに変換した例が見れます。ほんの2, 3箇所anyの注釈を入れているだけで、ほぼほぼそのままですね。実際のところ、↑みたいなゆるふわキャストなんて、たとえJSといえど多用してるわけがないので、作業的な手間はあまりありません。やることは、コンパイルエラーの出た箇所をポチポチとモグラたたきするだけなので、楽ちん。

実際にやってみる

理屈上はそうですが、実際やってみるとどうなんでしょうねえ、ということで、linq.jsでやってみました。(なお、linq.jsの型定義自体はlinq.jsのTypeScript対応とTypeScript雑感で手付けですでにやってあります)。まず.tsにしてコンパイルかけてみると、赤波線が全体に出てきてわけわからんオワタ!エラー90件!

で、まあ、こういう場合は問題は基底部分にあります。

(function (root, undefined) {
// 中略
})(this);

問題なのはundefinedです。function(root, undefined) として定義しているのに、(this)ということで、呼んでないから。冷静に見てみれば、ただたんにメソッド呼ぶ引数が足りないぜ、って言ってるだけですな。ちなみにこのコード自体は、undefinedは代入可能な代物で破壊されている可能性があるから(あるわけないけど!)、安全なundefinedを作ろう、という古臭いイディオムです。

エラーが90件もあってわけわかりませんが、一番最初のエラーが「Supplied parameters do not match any signature of call target」なので、やっぱり冷静に見てみれば、ちゃんと教えてくれていた、と。TypeScript優しいのね。

なのでundefinedを抜けば真っ赤っ赤はなくなります。OK。だがまだエラーは続く。というかエラー件数は89件になっただけである。

お次はEnumeratorがないぞ!というエラー。

if (typeof Enumerator !== Types.Undefined) {
if (typeof Windows === Types.Object && typeof obj.first === Types.Function) {

このEnumeratorはIEのみに存在するオブジェクトで、Windows Script Hostで列挙するのに使ったり使わなかったりする、今では知らない人のほうが遥かに多いであろう謎オブジェクトです。Windowsのほうも同様に、Windows8用アプリケーションにしか存在しません。さて、これへの対処は、定義ファイルのない外部ライブラリを使う際と同じで、anyでdeclareします。ファイルの先頭に

declare var Enumerator;
declare var Windows

と書いておけばOK。しかしまだまだエラーは続くよ!該当箇所はここ。

var Enumerable = function (getEnumerator) {
    this.getEnumerator = getEnumerator;
};

// このUtilsで赤線
Enumerable.Utils = {}; // container

このEnumerableが意図するところはコンストラクタです。new Enumerable()するためのものです。で、JavaScriptでは関数にもオブジェクトを生やせますが、TypeScriptでは生やせません。対処方法はまあ、面倒くさいのでEnumerableをanyにしましょう。

var Enumerable: any = function (getEnumerator) {
    this.getEnumerator = getEnumerator;
};

これだけで割と一気に解決します!89件あったエラーが残りほんの数件に!any最強!dynamic! で、linq.jsでは同じようにOrderedEnumerableとArrayEnumerableというものが存在するので、同様にanyにしておきます。

そんなわけで、なんとなくわかったと思いますが、ようするにエラーの出てるところを片っ端からanyにしていくだけです。ただしルート階層に近いものを優先的にany化すると、その下にぶら下がってるものは全部解決するので、意外とそんな手間じゃありません。

あとは一番下にAMD対応もどきのとこがあるのですが、これはそもそも微妙なのでまるごと削除して解決(てきとー)。で、対応はほんとこれだけです。あっという間だし簡単ですなあ。TypeScriptのJavaScriptとの互換性は本物だ!

declarationsオプション

で、ここからが本題であって本題ではないのですが、TypeScriptはtsc -declarationsとオプションをつけてコンパイルすると、d.tsを吐いてくれます。ちゃんと型定義されたtsファイルならちゃんとしあd.tsを吐いてくれます。役立ちです。

で、人間欲が出るもので、もしこれを、↑のように修正した.tsにかませてやるとどうなる?もし、たとえanyであっても定義テンプレを吐いてくれたら、そこから注釈入れてくだけですむわけで、随分と楽になりますよね?面倒くさい型定義よさようなら。

というわけで、こいつをdeclarationsオプションをつけてコンパイルしましょう。

tsc linq.js.ts -declarations

期待のlinq.js.d.tsの結果は

var Enumerator;
var Windows;

になります(笑)。はい、関数で丸ごと括った部分が消滅してしまいました。クソが。今回は定義ファイルが欲しいだけなので、関数で括る部分を除去して再度コンパイルすると

var Enumerator;
var Windows;
var Functions: { Identity: (x: any) => any; True: () => bool; Blank: () => void; };
var Types: { Boolean: string; Number: string; String: string; Object: string; Undefined: string; Function: string; };
var Utils: { createLambda: (expression: any) => any; isIEnumerable: (obj: any) => bool; defineProperty: (target: any,methodName: any,value: any) => void; compare: (a: any,b: any) => number; dispose: (obj: any) => void; };
var State: { Before: number; Running: number; After: number; };
var IEnumerator: (initialize: any,tryGetNext: any,dispose: any) => void;
var Yielder: () => void;
var Enumerable: any;
var OrderedEnumerable: any;
var SortContext: any;
var DisposableEnumerable: (getEnumerator: any,dispose: any) => void;
var ArrayEnumerable: any;
var WhereEnumerable: (source: any,predicate: any) => void;
var WhereSelectEnumerable: (source: any,predicate: any,selector: any) => void;
var Dictionary;
var Lookup: (dictionary: any) => void;
var Grouping: (groupKey: any,elements: any) => void;

外に出したくないもの(Yielderとか)は、まあ、あとで別途削除すればいいんですが、しかしそもそも肝心のEnumerableメソッドが全部出てないぞ! 理由としては、ようするにanyつけちゃったから。うーん、これじゃ実用度ゼロですね。

そもそもfunctionで定義したクラス(をコンストラクタとして使いたい)というのを、それがコンストラクタなのか関数なのかをどうやって区別するんだ?って話ですし、無理ですなー。(ファイル全てをなめてthis.してるのはクラスとか.prototype = hogehogeしてるのはクラスとか、曖昧な判定はできるでしょうけれど、それは危険ですしね)。

夢は見ちゃダメ。でもMicrosoftならきっといつかはやってくれるはず!(実際、GWT用に空気読んでJavaScriptからJavaの定義を吐いてくれるものは存在するとか)

まとめ

プレーンなJavaScriptはほぼほぼTypeScriptになります。素晴らしい互換性です!一方、型定義自動生成のほうは無理でした。地道に頑張りましょう。

あ、そうそう、今回の記事で言いたいのは別に表題通りの話じゃあないです。既存JSは既存JSとして使えばいいので、わざわざTypeScript化する必要なんて全然ありません。いえ、あります、ちゃんと型付けするならば。でも、今回のようにanyを付けて回る程度の話は全くの無意味です。じゃあどうでもいいかといえば、やっぱりそうじゃなくて、TypeScriptとJavaScriptの互換性というのはどういうものなのか、というとこは位置づけ的には大事ですからね、漠然とじゃあなく抑えておきたいところ。

Profile

Yoshifumi Kawai

Cysharp, Inc
CEO/CTO

Microsoft MVP for Developer Technologies(C#)
April 2011
|
July 2024

Twitter:@neuecc GitHub:neuecc

Archive