JavascriptはC#と違ってバグに気づきにくいです。
たとえばこんなコード。
1 2 3 4 5 6 7 |
function func1(num){ return num * 2; } function func2(num){ return (num * 2).toString(); } |
func1関数はnumber型を返します。これに対してfunc2関数はstring型を返します。そのため以下のふたつは異なる結果になります。
1 2 |
console.log(func1(1) + 100); // 2 + 100 なので 102 と出力される console.log(func2(1) + 100); // '2' と 100 の文字列を結合する処理がおこなわれるので 2100 と出力される |
また以下のようなコードを書いてもエラーになりません。
1 2 |
let a = func1("aaa"); // 引数はnumber型を想定しているのに文字列を渡している。 console.log(a); // 文字列のかけ算は定義されていないので NaN が返される。 |
関数の場合
このようなミスは実行時にならないとバグと気づくことができません。C#のようにコーディングのときに気づくことはできないのでしょうか? 実は //@ts-check を使うことでバグに気づきやすくなります。
1 2 3 4 5 6 7 8 9 10 11 12 |
//@ts-check /** * @param {number} num * @returns {number} */ function func1(num) { return num * 2; } let c = func1("aaa"); // "aaa"の部分に赤線がつきエラーであると気づくことができる console.log(c); |
//@ts-check は先頭に書きます。途中に書いても機能しません。
関数の前に
1 2 3 4 |
/** * @param {number} num * @returns {number} */ |
と書くことで、引数のnumはnumber型、戻り値もnumber型と指定することができます。なのでfunc1関数にnumber型ではないものを渡すとすぐに赤線がつきエラーであると気づくことができます。
このようなJSDocをつけることで関数の引数と戻り値の型をわかりやすくすることができます。ただし以下のような書き方では機能しません。/ のあとに * をふたつ以上つけなければなりません。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 |
// これはダメ // @param {number} num // @returns {number} // これもダメ /* * @param {number} num * @returns {number} */ // これはOK /** @param {number} num @returns {number} */ |
複数の引数がある場合は@paramの行を増やせばOKです。
1 2 3 4 5 6 7 8 9 |
/** * @param {number} num1 * @param {number} num2 * @param {number} num3 * @returns {number} */ function func3(num1, num2, num3){ return num1 + num2 + num3; } |
ただし以下のように同じ名前の関数を複数回定義した場合はエラーを指摘してくれません。console.log(func4(1))をどこで実行しても最後に定義された部分がこの関数の定義になります。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 |
console.log(func4(1)); // どこでfunc4(1)を実行しても結果は同じ 3 が出力される /**** * @param {number} num * @returns {number} */ function func4(num) { return num * 2; } console.log(func4(1)); // 同じ名前の関数で異なる内容の関数を定義してしまった /**** * @param {number} num * @returns {number} */ function func4(num) { return num * 3; } console.log(func4(1)); |
変数の場合
変数の場合は以下のようにします。変数 d はstring型です。ところがfunc1関数はNumber型を返すので間違いに気づくことができます。
1 2 3 |
/** @type {string} */ let d = func1(1); // Number型をstring型に代入しようとしているので d に赤線がつく console.log(d); // ただし実行は可能。 2 と出力される |
クラスの場合
クラスの場合はどうなるのでしょうか? いろいろ突っ込みどころ満載のクラスを定義してみました。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 |
class MyClass{ constructor(){ /** @type {number} */ this.X = 0; /** @type {string} */ this.Text = ''; this.Y = 0; this.abcde = 0; } Func1() { return this.X + 5; } // 同じ名前の関数がふたつ存在する Func1() { return this.X + 10; } /** * @param {number} num */ Func2(num) { this.X = num; } /** * @param {string} str */ Func3(str) { this.X = str; } } |
同じ名前の関数がふたつあります。この場合は赤線で指摘してくれます。ただし実行は可能でその場合は最後に宣言した関数が適用されます。
1 2 |
let myClass = new MyClass(); console.log(myClass.Func1()); // 10 と出力される |
Func2関数は引数がnumber型ですが、2回目の呼び出しではstring型を渡しています。この場合は’abc’に赤線がつき、引数の型が間違っていることを指摘してくれます。ただ実行しようとするとエラーにならず実行されてしまいます。これがJavaScriptの怖いところです。
1 2 3 4 5 |
console.log(myClass.X); myClass.Func2(0); console.log(myClass.X); // 0 と出力される myClass.Func2('abc'); // 'abc'に赤線がつく console.log(myClass.X); // abc と出力される |
Func3関数は引数がstring型ですが、これをnumber型のXに代入しようとしています。この場合はFunc3関数内のthis.Xに赤線がつき、引数と型があっていないことを指摘してくれます。ただこの場合も実行しようとするとエラーにならず実行されてしまいます。
1 2 3 |
console.log(myClass.X); // 0 と出力される myClass.Func3('abc'); console.log(myClass.X); // abc と出力される |
Yは/** @type {} */ がありませんが、0で初期化されています。だからnumber型です。なので
1 2 |
myClass.Y = 'xyz'; console.log(myClass.Y); |
と文字列を代入しようとすると赤線がつきます。ただしこの場合も実行可能で xyz と出力されます。
abcdeを書き換えようとしてタイプミスをしてしまったとします。これがC#ならそんなメンバーはいないとエラーが出るところですが、JavaScriptはエラーになりません。そしてタイポに気がつかず console.log(myClass.abcde);を実行。100 が出力されることを期待していたのに 0 と出力され、「値が0から100に変更されないのはなぜだ?」とバグ探しに無駄に時間を費やすことになるのです。
1 2 3 4 5 |
myClass.abced= 100; // 本当は myClass.abcde= 100; としたかった console.log(myClass.abcde); // 100 ではなく 0 と出力される console.log(myClass.abced); // この場合は100と出力される myClass.abced += 50; // 再びタイポ。意図せずメンバーを追加してしまった。 |
しかし//@ts-checkとしていれば、abcedの部分に赤線がつくので気づくことができます。
number型の配列と思っていたら…
数字をカンマ区切りでひとつの文字列にして関数に渡して、内部で配列に分解して処理をしたいときがあります。以下のコードは配列の全部の要素から1を引いたものを取得しようとしています。[ 0, 1, 2, 3, 4 ]と出力されることを期待していて、実際に[ 0, 1, 2, 3, 4 ]と出力されます。
1 2 3 4 5 6 7 8 9 |
let numbers = '1,2,3,4,5'; let ar1 = numbers.split(','); let ar2 = []; for(let i=0; i<ar1.length; i++){ ar2.push(ar1[i] - 1); } console.log(ar2); |
では以下の場合はどうでしょうか?
これは配列の全部の要素に1を加えたものを取得しようとしています。[ 2, 3, 4, 5, 6 ]と出力されることを期待しているのですが、実際にはそうはならず[ “11”, “21”, “31”, “41”, “51” ]となってしまいます。console.log(ar3)とやれば出力された要素が””で囲まれているので「しまった!文字列だった」と気づくことができますが、ar1[i]をさらにnumber型に代入したり引数として渡す場合はわかりにくいバグになります。
1 2 3 4 5 6 7 |
let ar3 = []; for(let i=0; i<ar1.length; i++){ ar3.push(ar1[i] + 1); } console.log(ar3); |
そこで以下のように書けばar3はnumber型の配列であると明確に定義することができ、ar3.push(ar1[i] + 1);と書いたときにar1[i]の部分に赤線がつき、すぐに間違いに気づくことができます。
1 2 3 4 5 6 7 8 9 10 11 12 |
/** @type {number[]} */ let ar3 = []; for(let i=0; i<ar1.length; i++){ ar3.push(ar1[i] + 1); // ar1[i]の部分に赤線がつく } // number型の配列に格納するのであればこう書くべき for(let i=0; i<ar1.length; i++){ ar3.push(Number(ar1[i]) + 1); } |
canvasを使った描画
//@ts-checkとJSDocを書いておくと型チェックができるだけでなくコード補完の機能も強化されるのでおすすめです。タイピングが苦手な方はぜひ先頭に//@ts-checkを書きましょう。
ただcanvasを使った描画をするとき、このコード補完の機能がうまく働いてくれません。
1 2 |
let can = document.getElementById('can'); let con = can.getContext('2d'); |
このあとcon.とタイピングしたときに候補になりそうな関数が表示されてくれると助かるのですが、なぜかうまくいきません。
1 2 3 4 5 |
/** @type {HTMLElement} */ let can = document.getElementById('can'); /** @type {CanvasRenderingContext2D} */ let con = can.getContext('2d'); |
これだと赤線がつきます。/** @type {HTMLElement} */ ではなく /** @type {HTMLElement | null} */ と書けといわれたり、getContextはHTMLElementには存在しないとかいわれます。
getContextが存在するのはHTMLElementではなくHTMLCanvasElementです。しかし/** @type {HTMLCanvasElement} */とすると赤線がつくのでここは仕方なく以下のように// @ts-ignoreで黙らせます。これで入力補完でgetElementByIdが表示され、con.と入力すると呼び出し可能な関数が表示されます。
1 2 3 4 5 6 |
/** @type {HTMLCanvasElement} */ // @ts-ignore let can = document.getElementById('can'); /** @type {CanvasRenderingContext2D | null | undefined} */ let con = can?.getContext('2d'); |
あとは描画のための処理を記述します。
1 2 3 4 5 |
if(con != null){ con.font = '30px MS ゴシック'; con.fillStyle = '#ff0000'; con.fillText('赤色で文字を描く', 100, 50); } |
複数のファイルにまたがった場合も型チェックやエラーチェックが可能
複数のJavaScriptを読み込むときにも対応させるには、以下のようにします。同じディレクトリにfoo.jsがある場合はこれで入力補完にfoo.jsで定義した変数や関数が表示されます。
foo.js
1 2 3 4 5 6 7 8 9 10 11 |
//@ts-check ///<reference path="./bar.js"/> let a = 0; class ClassFoo{ constructor(){ this.X = 0; this.Y = 0; } } |
bar.js
1 2 3 4 5 6 7 8 9 |
//@ts-check ///<reference path="./foo.js"/> let foo = new ClassFoo(); // ClassFooのメンバーへの入力補完が機能する let a = 1; // エラーfoo.jsですでに定義された変数が二重定義されている // このような場合も赤線がついて教えてくれる |