TypeScript の型定義に凝りすぎじゃね?
ここ数年で、Qiita や Zenn で TypeScript の話を見かける機会が多くなった。JavaScript には Java のような型定義がなく、初心者の混乱の元・ひいては障害の元になりうるのはよく分かる。
しかし、最近どうにもこうにも、TypeScript でむりくり型定義するような Tips を多く見かけて、疑問に思っている。たかが TypeScript に頑張り過ぎじゃね? と。
- 【TS】TypeScript 4.0 の新機能 - Qiita
- こういう新機能とか、追加されるのはいいんだけど、本当に便利なんか?と思ってしまう
まず、TypeScript による型定義は単なる Linter でしかない。コードが実行される時は基本的に JavaScript に変換され、TypeScript の構文で記した型定義は消失する。コーディング中に静的解析し、警告を出してくれるモノでしかないのだ。
JS・TS を書くということは、得てしてどこぞの API と通信し、その結果を利用して処理することが多いだろう。レスポンスデータに型定義する方法もあるはあるが、大変だ。同じチームが作っているバックエンドサーバなら、中身をよく知っていて型定義を流用できるから簡単に実装できるかもしれないが、他システムの場合はなかなかそうもいかない。
勿論、頑張って型定義をこしらえることに意味がないワケではない。コーディング中に、Typo や勘違いに気が付けるようになるし、強制力が増す。それは良いことだ。
しかし、自分が気になっているのは、その効果に対して、かかるコストが見合っていないんじゃないかという点だ。
こういう API がある、こういうレスポンスが来る、じゃあコレを TypeScript が持つ構文でどうやって表現しようか、あぁ新しい TS じゃないとこういう型定義は難しいな、Interface を用意してー、クラスを用意してー、入れ子になっているからー…。
なんというか、そもそもそういう複雑な型の考慮が必要になっているシステム設計が悪いんじゃないか? それ。
一つのプロジェクトを小さく作っておき、外部通信が最小限で済めば、レスポンスを型定義したい場合も、簡単なクラスを何個か定義するだけで良い。使うプロパティのことだけ考えれば良いだろう。
オブジェクトや配列をこねこねする関数に関しても、一つの関数を小さく作れば、引数と戻り値はほとんどがプリミティブ型で扱えるレベルに落とし込めるはずだ。引数の表現に複雑な型定義が必要な関数は、その関数の設計自体が何らか誤っている。もっと小さく分割してやろう。
プロパティの存在チェックは TypeScript の型情報だけで済む話ではなく、事前条件・事後条件として検査し、異常時はエラーハンドリングすべき事項だろう。API コールが絡むと、型が保証される場面は少ないと考えている。あまり他システムというモノを信じていないのだろう。
JS 初心者がつまづくのは
- Truthy・Falsy という概念 (全てが Boolean でみなせる)
- String と Number の取り違え
null
とundefined
の存在
くらいだろう。これら3つは簡単に覚えられる。これらの避け方を知っていれば、意図しない型変換による障害など防げるのだから、JS を普通に書ける自分個人は、ほとんど TS がなくても困らないと思っている。
- Falsy なモノを暗記。それ以外は Truthy。これらは曖昧等価比較により
if
や三項演算子でtrue
・false
とみなされ比較できる- 基本は面倒でも厳密等価比較を書くだけで、TS の型定義なしに比較がちゃんとできる
- 必ず
String
文字列にしたければ、次のようにテンプレートリテラルで再定義すれば良い。古くは+ ''
と空文字を繋げることで文字列にしていたモノだconst stringVariable: string = `${variable}`;
- 参考 : RokouchaさんはTwitterを使っています 「あと string にする時は String() でやる方が良いと思うけどなあ、+ '' とかテンプレートリテラルで string になるのは副次的な効果でメインは文字列連結なので」 / Twitter
- 2021-02-10 追記 : ↑ のような指摘をいただいた。確かに元の値をそのまま使っていることが分かりやすいのは
String()
の方かも
- 必ず
Number
数値にしたければ、Number()
コンストラクタに入れてNumber.isNaN()
で判定すれば良い。数値じゃないと困る時は何らかエラーハンドリングが必要だろうconst numberVariable: number = Number(variable); if(Number.isNaN(numberVariable)) throw new Error('Variable Is Not A Number');
- 参考 : RokouchaさんはTwitterを使っています 「https://t.co/05QLJsvBdt 文字列を number にするのに Number() 使うのよくなくね?と思った、自分も前やってたけど Number.parseInt(value, 10) でやらないと事故が起きる」 / Twitter
- 2021-02-10 追記 : ↑ のような指摘をいただいた。自分はコレで問題になるコードを書かないように避けられて来たから気にしていなかったのだが、確かに
Number.parseInt(value, 10)
の方がより安全だし、意図が正確に伝わると思う
null
とundefined
だけは曖昧等価比較を使って良い場所 (ESLint などでも== null
だけは許容されるようなルールがある)。次のように書けばnull
とundefined
をまとめて除外できる。これらに意味付けしてnull
とundefined
を頑張って区別しようとするとつらいので、どちらもいっしょくたに扱い、「空である」ことは他の方法で表現した方が良いif(variable == null) console.log('variable は null か undefined です');
自分はコレだけ意識しておいて、一つひとつのプロジェクトを小さく作るようにしている。仕事ではチーム開発ないしは引き継ぎが発生するので TypeScript を使うが、外部連携の部分では as any
で逃げることも多々ある。ココを厳密に型定義できれば安全性が上がることは上がるのだが、外部システムを見ずにフロントだけ改修してバグを生むことにもなるので、そういう場面では any
で逃げつつ、その外部システムのドキュメントへのリンクなどを、ドキュメンテーションコメントで細かく書いて残している。「any
を使っている箇所は危なっかしい」というのは共通認識だろうから、それを利用して、「外部システムのことを考えて直してね」という気付きを与えるために使用している。
個人では TypeScript はほとんど使っていない。毎度 package.json
に typescript
が登場したり、tsconfig.json
や .eslintrc.json
での調整が必要だったりするのが、クソ面倒臭いのだ。上述のようにハマりやすいポイントは JS オンリーで簡単に避けられるので、サッサと直接 node
コマンドで実行できる、ないしはブラウザで直接実行できるように、素の JS で書いてしまう。TypeScript は IDE (というか VSCode) での補助機能を当てにした連携も多く、既存の巨大なコードを直す場合はそうした恩恵に預かれるのは良いが、そもそもそんな巨大なコードを書くなということである。
JS のハマりポイントの代表例だけ知っておき、システムを小さく作っておけば、TypeScript なんて大仰に使う必要ないのだ。力を入れるべき場所がズレているように思う。
2021-02-10 追記 : 反応に対する反応書いた。