もうTypeScriptの補助輪を外そう 明日は//@ts-checkを使う
※近所(同じ港区内)のあのIT企業さんからお声をかけていただきました。登壇予定です。
※某商業誌さんから執筆依頼がありましたが、有料ですと気が引けるのでお断りしました。
Vimで書いている人はもういないから、そろそろTypeScript(自転車の補助輪)を外していこうという話。
Visual Studio Code は下のリンクからインストールできる。これが前提。// @ts-check
が利用できる。
※Denoの話を知らない人は、これを読むと良い。
DenoがTypeScriptの使用をやめる5つの理由
(Denoは、Node.jsの作者であるライアン・ダールによって作成され、V8 JavaScriptエンジン及びRustプログラミング言語に基づいた、JavaScript及びTypeScriptのランタイム環境である。セキュリティと生産性に焦点を当てている)
※TypeScriptのESMについては、これを読むと良い。
TypeScript 4.5 以降で ESM 対応はどうなるのか?
(とてもタイムリーな話題だ)
これらは氷山の一角に過ぎない。
あなたのリーダーにこの話をしたとき、言葉を濁されたり、笑い飛ばされるようだったら、あなたのプロジェクトは破滅への道を進んでいるのかもしれない。
SNSでの注意点だが、情報商材屋が組織を作っているので、気をつけてほしい(エンジニア採用で問題となっている)。
銀の弾丸と呼ばれた補助輪に暗雲が立ち込めている。今後、脱TypeScriptの加速は避けられないが、段階を踏んで安全に進むべきである。
TypeScriptを活用したJSプロジェクト
明日からはJavaScriptファイルにおける //@ts-check
を使用してほしい。
JSDocを使ってJSで型ヒントを提供する
.jsファイルでは、多くの場合型を推測することが可能。型が推測できない場合、JSDoc構文を使って指定することができる。
宣言の前でJSDocのアノテーションを使い、その宣言の型を設定する。
例えば
/** @type {number} */
var x;
x = 0; // OK
x = false; // OK?!
前述のコードサンプルの最後の行はTypeScriptではエラーとなりるが、JSプロジェクトのデフォルトではエラーを発生しない。
JavaScriptファイルでエラーを有効化するには、.jsファイルの最初の行に //@ts-check
を追加して、TypeScriptにエラーを発生させるようにする。
// @ts-check
/** @type {number} */
var x;
x = 0; // OK
x = false; // エラー
Type 'boolean' is not assignable to type 'number'.
エラーを追加したいJavaScriptファイルがたくさんある場合は、jsconfig.jsonを使用するように変更する。 ファイルに //@ts-nocheck
コメントをつけることで、ファイルのチェックを省略することができる。
納得できないようなエラーは前の行に //@ts-ignore
または //@ts-expect-error
を追加すれば良い。
// @ts-check
/** @type {number} */
var x;
x = 0; // OK
// @ts-expect-error
x = false; // エラー
JSDocリファレンス
以下のリストは、JavaScriptファイルの型情報を提供する JSDocアノテーションにおいて、現在サポートされている構文の概要。以下に明示的にリストに入っていないタグ(@asyncなど)はまだサポートされていないことに注意。
@type
@param (or @arg or @argument)
@returns (or @return)
@typedef
@callback
@template
@class (or @constructor)
@this
@extends (or @augments)
@enum
class拡張
プロパティ修飾子 @public、@private、@protected、@readonly
タグの意味は通常、jsdoc.appで与えられたものと同じか、あるいはそのスーパーセットになる。
@type
“@type”タグを使用すれば、型名(プリミティブ、TypeScript宣言やJSDocの”@typedef”タグで定義されたもの)を参照することができる。ほとんどのJSDoc型と、stringのような最も基本的なものからConditional Typesのような高度なものまで、あらゆるTypeScriptの型を使うことができる。
/**
* @type {string}
*/
var s;
/** @type {Window} */
var win;
/** @type {PromiseLike<string>} */
var promisedString;
// DOMプロパティを使ってHTML要素を指定することができる。
/** @type {HTMLElement} */
var myElement = document.querySelector(selector);
element.dataset.myData = "";
@param @returns
@paramは@typeと同じ型の構文を使用するが、パラメータ名を追加する。また、パラメータ名を角括弧で囲むことで、パラメータを任意のものとして宣言することもできる。
// パラメータは様々な構文形式で宣言することができる
/**
* @param {string} p1 - 文字列パラメータ
* @param {string=} p2 - 任意のパラメータ(Closure構文)
* @param {string} [p3] - 任意のパラメータ(JSDoc構文).
* @param {string} [p4="test"] - デフォルト値を持つ任意のパラメータ
* @return {string} 結果
*/
function stringsStringStrings(p1, p2, p3, p4) {
// TODO
}
@typedef @callback @param
複雑な型を定義するために@typedefを使うことができる。@paramを使った同様の構文でも動作する。
/**
* @typedef {Object} SpecialType - 'SpecialType'という名前の新しい型を作成
* @property {string} prop1 - SpecialTypeの文字列プロパティ
* @property {number} prop2 - SpecialTypeの数値プロパティ
* @property {number=} prop3 - SpecialTypeの任意の数値プロパティ
* @prop {number} [prop4] - SpecialTypeの任意の数値プロパティ
* @prop {number} [prop5=42] - SpecialTypeのデフォルト値を持つ任意の数値プロパティ
*/
/** @type {SpecialType} */
var specialTypeObject;
specialTypeObject.prop3;
@template
ジェネリクス関数は@templateタグを使って宣言することができる。
/**
* @template T
* @param {T} x - 戻り値に流用するジェネリクスパラメータ
* @return {T}
*/
function id(x) {
return x;
}
const a = id("string");
const b = id(123);
const c = id({});
@constructor
コンパイラはthisプロパティの代入に基づいてコンストラクタ関数を推測するが、@constructorタグを追加すればより厳密なチェックとより良い提案を受けることができる。
/**
* @constructor
* @param {number} data
*/
function C(data) {
// プロパティの型は推測される
this.name = "foo";
// あるいは、明示的に設定することもできる
/** @type {string | null} */
this.title = null;
// また、他のところで設定されている場合は、単に型注釈をつけることもできる
/** @type {number} */
this.size;
this.initialize(data);
Argument of type 'number' is not assignable to parameter of type 'string'.
}
/**
* @param {string} s
*/
C.prototype.initialize = function (s) {
this.size = s.length;
};
var c = new C(0);
c.size;
var result = C(1);
Value of type 'typeof C' is not callable. Did you mean to include 'new'?
@this
コンパイラは通常、thisが用いられるコンテクストからthisの型を推測することができる。推測できない場合、@thisを使って明示的にthisの型を指定することができる。
/**
* @this {HTMLElement}
* @param {*} e
*/
function callbackForLater(e) {
this.clientHeight = parseInt(e);
}
@extends
JavaScriptクラスがジェネリクスの基底クラスを拡張するとき、型パラメータが何であるべきかを指定するところはない。@extendsタグはそのような型パラメータを指定する方法を提供している。
/**
* @template T
* @extends {Set<T>}
*/
class SortableSet extends Set {
// ...
}
@enum
@enumタグを使うと、すべてのメンバが指定された型であるオブジェクトリテラルを作成することができる。JavaScriptのたいていのオブジェクトリテラルとは異なり、明示されていないメンバは使用できない。
/** @enum {number} */
const JSDocState = {
BeginningOfLine: 0,
SawAsterisk: 1,
SavingComments: 2,
};
JSDocState.SawAsterisk;
サポートされていないタグ
TypeScriptはサポートされていないJSDocタグを無視する。以下のタグは、サポート目標。
@const
@inheritdoc
@memberof
@yields
{@link …}
さあ、補助輪を外そう。
※せっかくコメントをいただいたのに、Zennの運営が勝手に削除したようです。とても残念です。
Discussion
あなたの意見に同意します。私が尊敬するWeb App Developerも同じ考えです。
天才であれば、この補助輪(TypeScript)は邪魔でしょう。外すことを薦めます。
しかし、天才でなければ、この補助輪を使用しない理由はないでしょう。
関連記事を書きました。
Deno が内部コードで TypeScript を辞めた件について誤解や憶測が広がっていると思いますので、ここにその決定をしたドキュメントの一部を DeepL で翻訳したものを引用します。
私の指摘の後にこの一文が追記されているのを確認したので反論させてください。なお人格攻撃をするつもりはなく、間違えを指摘するつもりです。初心者による技術記事執筆のすすめを参考にしています。
私の観測範囲では脱 TypeScript はそもそも起きていません。少なくとも Deno はかなり特殊な例であって脱 TypeScript の一例としてあげるのは間違っています。Deno の内部コードは通常の Web アプリケーションとは違い、以下のような特徴があります。
Object.create
やObject.setPrototypeOf
などを使わざるをえないケースがある[2]。この場合 TypeScript のベストプラクティスに当てはまらないunknown
だらけになってしまうTypeScript は辞めるべきであるという結論ありきの議論になっていると思います。今一度確認していただけると幸いです。
Deno のコントリビューターである kt3k さんの記事 Deno が Node.js に依存しなくなった を参考 ↩︎
例えば ReadableStream の実装 では %AsyncIteratorPrototype% を扱うために
Object.setPrototypeOf
を使っている ↩︎例えば
Math.abs
は ECMAScript の仕様としては引数にstring
を入力した場合にnumber
に動的型変換するが、型定義においては暗黙的型変換を防ぐためnumber
しか受け取らないようになっている。言語仕様に近いところではこのように実装と型定義の乖離が起きる ↩︎余談ですが私は Stage 1 Float16Array を実装したライブラリを公開しています。
ここでは JSDoc による型アノテーションを使った JavaScript ファイルと型定義ファイルを別々で管理しており、TypeScript は使っていません。これは Deno の例と同じように ECMAScript の仕様に厳密に準拠する場合は TypeScript は適さないと判断したからです。
TypeScript を採用するべきかどうかはプロジェクトに依存します。しかし(どのようなプロジェクトに対して脱 TypeScript を提唱しているのか私にはわかりませんが)通常のアプリケーションを開発するにあたっては TypeScript をあえて使わない理由はないと思っています。
何かと叩かれているのを見たのですが、JSDoc だけでも TS の恩恵はほとんど得られると思いますし、この記事が TS 不要論であるようには見えません。そういう体制の OSS に最近 contribute したこともありますが普通に快適でした。(
.js
+.d.ts
) からこの記事のように (.js
(JSDoc) →.d.ts
生成) へシフトするのは良いと思います。両者の無視できない非常に大きな違いの一つは
as
が明示的か暗黙的かというところでしょう。基本的に高級な領域で書く場合はas
がほとんど現れず、as
というのが「注意せよ(=書いた本人が妥当性を証明・保証せよ)」という目印になるから自分は TypeScript (.ts
系) を使います。Deno の件は、コア部分であるがゆえに、TS の作り上げた文脈に乗り切らず
as
が多くなりすぎるのが容易に想像つきます。そういうのを JSDoc に切り替える基準として考えようとするのはいいことだと思います。しかしたいてい、アプリケーションに該当するものはそうはならず、もし繰り返しas
が現れたなら、それはその箇所を関数に収めてその関数をテストすることに注力することで解決すると思います。(しかしこの記事が出るまで私はこれほど簡潔に TS vs JSDoc を説明できたとは思えないので、その点についてありがたく思います。)
(完全に余談ですが私は何年も、そして現在も TS を完全に vim のみで書いています)
良記事をありがとうございました!
JSDoc をきちんと書くだけでビルドの待ち時間がゼロになるのは快適やと思いまする。
TypeScript はビルドが遅いという誤解が広がっていると思われるのでコメントを残します。
決してビルドが遅いわけではなく、tsc を使った型チェックに時間がかかるだけです。これは TypeScript を採用した場合でも JSDoc を使った JavaScript を採用した場合でも同様です。
単に TypeScript から JavaScript に変換するだけであれば基本的に型アノテーション部分を取り除くだけで済むため低コストです。TypeScript の公式ドキュメントに記載してあるように Babel を使って変換することができます。特に最近は swc そしてバンドルするなら esbuild といった選択肢もあります。
JSDoc ならそのまま実行できるためビルド不要というメリットがあるのはわかりますが、そもそもビルドを一切することなくアプリケーションを開発するケースというのはかなり限られていると思います。Node.js でのみ使われるアプリケーションか、バンドラーが必要ない小規模な Web アプリケーションか、またはモジュールを使用していないクラシックな環境か。
どういうプロジェクトについて述べているのか前提がないためわかりませんが、今一度再考していただけると幸いです。
今度は TypeScript の Node.js ES Modules 対応についての記事が追記されていますね。その記事の中身を吟味することなく不用意に TypeScript へのネガティブキャンペーンをしてしまっていると思われます。その中身を一部引用します。
Node.js は従来の CommonJS スタイルの互換性を保つ決定をしたため ES Modules 対応がかなり複雑化してしまいました。Node.js v12 が Maintenance LTS となったことでサポートするすべてのバージョンで ES Modules が使えるようになり、TypeScript でもその対応に向けての実装や議論が始まったという認識でいます。
まだ対応が不十分だというのはわかりますが一朝一夕で解決される性質のものではありません。そもそも Node.js の ES Modules 対応は急を要するものではなく、npm へライブラリを公開している人たちが互換性を保ちながら少しずつ移行するものとなっています。記事にも述べられているように一般のアプリケーション開発者が気にするべき話題ではありません。