1 ポイント 投稿者 GN⁺ 2023-07-09 | 1件のコメント | WhatsAppで共有
  • TypeScriptのランタイム型情報を出力する必要性を主張し、この問題を回避するための Type Mapping、Code Generation / External Tool、Adapter プロジェクトを集めた一覧
  • 核心的な問題は、リフレクティブな型システムなしでシリアライズと検証を処理しようとすると、終わりのないボイラープレートや、スキーマファイルベースの特注コード生成が必要になる点
  • 回避策として io-tszod などが提示されるが、型をライブラリごとの方式で再宣言しなければならず、ライブラリ側が TypeScript のすべての型機能をサポートできないという不便さがある
  • TypeScript の型消去は、JavaScript プロジェクトが TypeScript の知識なしに出力された JavaScript を利用できるという利点として認めつつも、コードと分離されたルックアップテーブル形式で型情報を出力できるはずだと主張
  • デコレーターで解決しないでほしいという要望とともに、インターフェース利用や外部ライブラリ型のサポートのために、typescript.generateRuntimeType<T>() のようなコンパイラ認識の高階関数、F# Type Providers、C# Source Generators のようなアプローチを提案
  • 関連する既存の議論として 8年前の GitHub issue にリンクし、同じ問題を経験したプロジェクトであれば、星の数に関係なく PR を送って一覧に追加してほしいと呼びかけている

1件のコメント

 
GN⁺ 2023-07-09
Hacker News のコメント
  • 何を求めているのか理解するのに問題文を4回くらい読み直す必要があり、この手の事例こそ 簡潔な文章 がなぜ重要かを示している
    実際の要求はランタイム型安全性のように見えるが、TypeScript は JavaScript の代替/追加ランタイムにはならないという一線を引いてきたので、実現の可能性は低そうに見える。TypeScript は JS にコンパイルして役目を終えるのであり、その後 V8 などで起きることは守備範囲外だ。
    「TypeScript がランタイム型情報を出力すべきだ」という要求は、現在の TypeScript とはかなり異なる 新製品 を求めているのに近い

    • ランタイム型安全性そのものを求めているというより、コンパイル時点で型をリフレクションして値を生成し、その情報をランタイムで使えるようにしてほしいという要求に近い
      たとえば任意のインターフェースとオブジェクトを受け取って検証する汎用 validate 関数を作れたらよい、ということだ。コンパイル時リフレクションで型ごとの JS 検証コードを生成したり、ランタイムに T を引数として渡して比較したりする方法はあり得るが、現在の TypeScript の哲学では難しいと思う
    • タイトルを見ただけで昔から欲しかった機能だとすぐ分かり、より公式でよくサポートされた https://github.com/rbuckton/reflect-metadata を求める話に近いと思った
      TypeScript はコンパイル中に多くの型情報を把握しているが、コンパイルが終わるとその情報を捨ててしまう。この情報をファイルとして出力したり Reflect にメタデータとして残したりできるのに捨ててしまうのは、特に「すべてがオブジェクト」である JavaScript の性質を考えると惜しい制約だ。
      正確な TS の型安全性までは得られないとしても、getter/setter や ES2015 Proxy で一部の検査を差し込めるし、型情報をランタイムに残せば 実行可能なメタデータ としてさまざまな面白い可能性が開ける
    • TypeScript がコンパイラだから不可能だというのは正しくない。型情報を JS オブジェクト として出力し、ランタイムで参照できるリフレクションライブラリをサポートすればよい
      すでに TS の enum は JS オブジェクトとして出力され、ランタイム参照が可能だが、文字列リテラルのユニオン型はそうではない。また TypeScript は 型ガード関数 をサポートしているので、型システム内の情報を使ってこうした関数を自動生成するのも、それほど難しくはなさそうに見える
    • ざっと読んだだけでも要求は明確だった。TypeScript が型消去の過程で把握した型情報を、生成された JavaScript の横にある 補助チャネル として出力してほしい、ということだ
      PDB ファイルのような比喩で考えればよい。TypeScript がすでに持っていて捨てている情報なのだから、まったく新しい製品が必要なわけではない
    • README の冒頭に「7年前の GitHub イシュー」へのリンクがあり、そこで問題がもっと直接的に説明されている
      https://github.com/microsoft/TypeScript/issues/3628
  • TypeScript の PM の立場からすると、その欲求は理解できる。データ検証では ランタイム型検査 がよく必要になり、その隙間を埋めるためのライブラリも多い
    ただし、設計判断の異なるライブラリがこれだけ多いという事実自体、この問題が明確な正解を持つ解決済みの問題ではないことを示している。TypeScript の初期設計時からそれは認識されており、この原則はよく保たれてきたと思う。
    その代わり TypeScript は、ランタイム型検査ライブラリが実際に行うことを型として正確に表現できるほど強力になっており、ユーザーは API を通じて型からランタイム検証ロジックを組み立てられる。これくらいが合理的な柔軟性に感じられる

    • プログラミングを始めてまだ日が浅いのだが、TypeScript がなぜもっと精巧な ユーザー定義データ型 を作れないのか気になる
      たとえば、特定の範囲内の数値や郵便番号パターンを満たす文字列を型として定義し、コンパイラが普通の関数のように書かれた検証関数を使って妥当性を確認する言語を想像している。こうした機能がないのは不要な複雑さを避けるための設計判断なのか、それとも性能のような技術的制約によるものなのか気になる
    • TypeScript コンパイラに公式の プリプロセッサプラグイン があれば役に立つかもしれない
      今でも必要な人は tsc に渡す前に、型情報からランタイムオブジェクトを生成するプリプロセッサを作れるが、努力がばらばらに散っている。公式のプラグイン型プリプロセッサとそのエコシステムがあれば、よい解決策が見つかるかもしれない
    • Microsoft が TypeScript プラグインや最上位ラッパーとして MacroScript をホスティングすれば、この問題は解けるかもしれない
      火をつけるだけでコミュニティが保守を手伝うだろうし、クライアント生成からランタイム型アサーションまで、さまざまなコード生成の要求を解決できる
    • コンパイル後に型情報を Class オブジェクトに残そうという別の提案については、どう思うのだろうか
  • そうしないのには理由がある。TypeScript が JavaScript 上のランタイムのようなものになり、JS にコンパイルされる新しい言語になってしまう。
    今の TypeScript は型注釈付きの JavaScript に近い。JS にコンパイルされる言語はすでにたくさんあるので、そのどれかを使えばよい。ランタイム型のある TypeScript を望む人たちは、JavaScript よりも Java/OOP スタイルのコードを書きたがっているように見えるが、JavaScript は動的型付け言語であり、その点も利点だ

    • 必ずしもそうではない。generateTypeInfo!() のようなマクロFoo 型をエンコードした JS オブジェクトに展開される形なら、依然として読みやすい JavaScript にコンパイルされる。
      ただしこのマクロは、「TS = 型注釈付きの JS であり、コンパイルは注釈を削るだけでよい」という性質を壊す。Foo の構造的型を実際に計算しなければならないからだ。
      また TypeScript の型は構造的なので、ランタイムでオブジェクト構造を検査すればある程度型を知ることはできるが、文字列ユニオンに属するかどうかのような消去された名目的情報や型名を要求すると、TypeScript 型システムのチューリング完全性と暗黙の構造変換のため、すぐに難しい問題になる
    • 両方持つことはできる。Lisp のホモイコニシティのように、コードとデータが近い世界を思い描ける。
      真面目に言えば、ランタイムがなくても型をデータとして露出すればリフレクションは得られる。多くの開発者がこれを真似しようと苦労してきたのを見ると、やらないほうがむしろ愚かに見える
    • その理屈はよくわからない。TypeScript はすでに JS にコンパイルされる新しい上位互換言語だ。
      ランタイム型は、コンパイル用の型システムとデータ検証用の型システムを別々に重複して維持しなければならない問題を解くもので、OOP と直接の関係はなさそうだ。この問題の回避ライブラリとして好まれている io-ts も関数型プログラミング寄りに強く依存している
    • 十分に大きいプロジェクトでは動的型付け言語はよくなく、NaN のような予期しない値に何度もつまずく、もつれた災害に近い。
      私が見てきたワークフローでは、TypeScript はすでに JS にコンパイルされる言語なのだから、どうせならその利点を最大限活用したほうがよいと思う
    • TypeScript は単なる型注釈付き JavaScript ではない。モードのひとつではありうるが、古い JS 構文へ出力する機能もあり、元の TS コードとまったく違って見えることもある。
      型注釈 + Babel に近く、そこに非常に便利な機能をひとつ足すのはよい考えだ。JSON.parse した文字列を型付き構造へ安全に変換できないのは奇妙で、ほかの言語ではたいてい可能だ
  • 以下の内容より先に言っておくと、これは十分議論に値する妥当なテーマであり、客観的に唯一の正解があるとは思わない。
    TypeScript は、Enum がオブジェクトを出力する例外を除けば JavaScript 上の選択的レイヤーであり、TS コードは型だけ消せば JS になる。この原則を大きく外れないなら、実際の要求は型からシリアライザ/バリデータを作るライブラリに近い。そうしたライブラリはすでに多くあり、結局のところそのうちのひとつを公式の標準的選択肢にしようという要求に見える。
    個人的には、TypeScript の中核言語にランタイムリフレクションが入ることは望まない。ランタイムにはただ JavaScript があるだけで、ソースマップがなくても出力 JS が読みやすくデバッグ可能である点を大事にしている

    • Enum 以外にもランタイムコードを出力する例外はあり、その多くもやはりミスだと見なされている。代表例として modulenamespace がランタイム構造だった。
      ただしここ数年は主に TypeScript 自身の中で使われていたし、TypeScript も最近はそこから離れている。さらにかなり人気のある例外としてパラメータープロパティがあり、コンストラクター引数でクラスメンバーの型を定義する構文で、重複するボイラープレートを減らすためあまり批判を受けなかったようだ
    • 「TS コードは変換なしで JS になる」という話は、言語全体と型表記をそう捉えるなら成り立つ話だ
  • TypeScript の神々に懇願するより、JavaScript 側と取引して型検査を JS に入れるほうがよいのではないかと思う。
    ランタイム型が必要なら型ガードを使い、頻繁に必要なら io-tszod を使って型をバリデータ/コーデック/スキーマとして記述する。JavaScript に合意された方法が生まれるまでは、TS 仕様、型チェッカー、コミュニティがランタイム検証を背負うべきではないと思う

    • 完璧ではないにせよ、zod でスキーマとバリデータを書き、そこから型を導出する方式にはかなり満足している。検証が型の変化とともに進化する点がよい。
      こうした機能は言語に入るべきだと感じる人もいるが、ライブラリごとに議論の余地がある設計選択が多いので、プロジェクト要件に合わせて実装を選べるほうがむしろよいかもしれない。
      ただし zod のランタイム検証とスキーマベースの型導出のようなパターンは、ts-pattern のパターンマッチングと必ずしもうまく噛み合わない。こうしたライブラリの型定義は迷路のようで、うまくいきそうなコードが期待どおりに動かないことがある。ランタイム安全性と網羅性検査付きパターンマッチングが滑らかに結び付けば非常によいと思う
    • JavaScript に本当にそういう機能が入るのがよいことなのかは大いに疑問だ。
      Promise、デコレーター、新しいパイプ演算子はいずれも実装の方向性を誤ったと感じる
  • TypeScript 開発者のひとりが、やり直せるなら enum は入れなかっただろうと言っていたのを覚えている。enum がランタイムコードを出力する唯一の機能だからだ。
    TypeScript はランタイム動作を変えず、TypeScript 専用の {#if} のようなものもない

    • その話は事実だ。その理由が唯一の理由だったかはわからないが、確かに理由のひとつではあった。
      ただし TypeScript には「JavaScript + 型注釈」を超える機能がいくつかある。namespace、コンストラクター引数でクラスプロパティを定義する構文、古い実験的デコレーター、コンパイル時に消えるが単に JS 関数に型注釈を付けた以上のものに見える this パラメーター構文などだ
  • よりよいタイトルは「TypeScript、リフレクション/ランタイム型を提供してほしい」に近い。
    現在の最善の解決策はおそらく emitDecoratorMetadata だろう。 https://www.typescriptlang.org/tsconfig#emitDecoratorMetadata

  • 筆者はTypeScriptの設計目標を誤解しているように思える。目標は複雑さのないクリーンなJS出力を作ることではなく、TypeScriptのランタイム意味論がJavaScriptと同一に保たれるようにすることだ。
    残念な例外であるenumを除けば、TypeScriptは型注釈を取り除くだけでJavaScriptになる。TypeScript周辺のエコシステムは完全な型消去を前提としており、この要求が受け入れられるとESBuild、Deno、BunのTSサポートはほぼ不可能になるかもしれない。それぞれがtsc全体を自分たちの言語で再実装しなければならないからだ。
    一方で、OPが不満を述べている複雑なライブラリ群はユーザー空間で実装されているため、こうしたツールと互換性がある

    • ランタイムなしでも静的にランタイム型情報を吐き出す方法は数多くある。keyofをクラスのキー一覧に変換したり、TSクラスがES6クラスのようにキーをundefinedで初期化するようにしたりできる。
      現在のTypeScriptクラスは、明示的に定義していないキーをすべて消してしまうため、新しいインスタンスにObject.keys()を呼んでも何も出てこない。tsconfig.jsonのオプションでTSクラスをES6クラスのように変換するか、ES6クラスをTS内でそのまま併用できるようにするだけでも、コード生成はずっと容易になる。
      また、tstypeof Foo::barのような静的構文が"string"にコンパイルされるなら素晴らしいだろう。基本的なRTTIだけでも、汚いボイラープレートなTypeScriptコードはすぐにかなり減らせるはずだ
  • この記事はうれしい。2018年にTypeScriptを始めて、2年ほど経つうちに、これは本質的に完全な解決策ではないと考えるようになった。
    Haskell/C#/F#から来たが、TSは強力な型システムを持っていても、開発中の一部を除けば、それらの言語が与えてくれる利点をあまりもたらしてくれない。現実世界を扱う際にはC#より実用上かなり制約が大きい。コンパイル後に検査なしでJSに落ちるという点を知らなければ、静的型を名乗るツールでは起こりえないはずのバグを避けるために、漏れる抽象化を常に意識しなければならない

    • TypeScriptの目的は常にWeb上で動くことであり、C#に対する言語的優位を与えることではなかった。
      .NETを開発するならC#を使い、Webを開発するならTypeScriptを使う、という話だ。C#は公称型付けで、かなりまともな実体化ジェネリクスを持ち、TypeScriptは構造的型付けで、健全性を犠牲にする代わりに複雑な型関係を表現できる強力な型システムを持つ。
      両者を組み合わせた言語が良いものになるかは疑わしく、2つの言語はそれぞれ異なる方向ではうまく機能するが、その方向性は互いにあまり噛み合っていない
  • ランタイムデータ検証にはhttps://zod.devを使っていて、完全に満足していた。
    別個の公称型をわざわざ定義せず、流暢なAPIでその場で意図を表現できるのはかなり良い

    • 公称型には別の用途がある。
      簡単な例では、ドメイン内で数値の2と通貨金額としての2を区別するために必要だ