TypeScript 不満点まとめ
- カテゴリ:
- その他プログラミング言語
- コメント数:
- Comments: 1
◆ 色々
ブログには何度か書いてますが 個人的には静的型付け言語が好きではないので TypeScript は極力使いたくないです
しかし 最近では TypeScript を使って書かれているものが増えてるので それらをいじるときなど 仕方なく触らざるを得ない機会も増えています
不満点を感じるたびにメモに書いていたら 1000 行近くになっていたので記事にまとめてみることにしました
一部は再確認のために最新版で試すと改善されてるのもあったので改善済みのもあるかもしれません
実行時は JavaScript なのでその速度は基本は変わりませんが JavaScript にするまでの処理や初回の準備などがあるので開発の速度は遅くなります
また一部は TypeScript の都合で不要な処理を追加する必要があり それはそのまま JavaScript になるため JavaScript のムダな処理が増えます
ほとんどは誤差程度ですが JavaScript よりは遅いことに変わりありません
一番の問題の型チェック部分の速度ですが かなり遅いです
ゲーム用途ならともかく 一般用途としてはそれなりにハイスペックの PC でも目に見えて遅いです
一番適してそうな VSCode を使っているのに JavaScript に比べてかなりの遅さです
構文のエラーや型エラーで赤色の線が出たりしますが それが出たり消えたりするのに何秒も待たないといけません
ひどいときは何分待っても反応しないのでコマンドから TS Server の再起動をして解決したりするほどです
ただ常にそんなに遅いかというとそうでもなく ちょっと動作確認のために数十行のコードを書くとかならほぼ一瞬で JavaScript を書いてるときと違いを感じません
ですが ライブラリを色々入れて大きめのものになってくるととても遅くなってきます
標準的な TypeScript ツールが JavaScript で動いていてシングルスレッドだからでしょうか
それに加え できる限り型は書きたくないので 積極的に推論に任せているのでさらに遅くなっているのかもですね
TypeScript コンパイラを Go や Rust で作るような話も見かけたのでそれらによってはマシになるかもしれません
JavaScript で十分な人からすれば余計な型情報を書かないといけないです
見ればこういうことだろうと人なら分かる部分でも 機械的に解析できないといけないのでコンパイラのためにあれこれ工夫しないといけないところが出てきます
私が静的型付けが嫌いかつ慣れてもないという部分もありますが JavaScript でコードを書いてから TypeScript コンパイラを通すために型情報を追加で書いていると ある程度複雑なところになると JavaScript でコードを書いたのとほぼ同じくらいの時間がかかってます
シンプルな計算のコードで型も単純なものならともかく React で UI を作るみたいに複雑なのになってくると 型定義も複雑になるので JavaScript のコードを書いてる時間以上に型エラーにならずにコンパイルできるようにするのに時間を使ってる気がします
複雑なジェネリクスや & や | で型を組み合わせたものが入ってくると型定義だけで何行にもなり 処理が見づらいです
行が分かれていればまだマシで JavaScript としては不要な as キャストなども入っていると見やすさは最悪です
C# や Java みたいなごちゃごちゃしたコードになってきます
読むだけなら型情報はなくても全く困りません
動いてるコードなんだから .foo プロパティにアクセスしていれば .foo があるのはわかります
多くの場合 それの具体的な型がなんであるかは知る必要がないです
型情報を消して JavaScript として表示して マウスオーバーで型が表示されるようなリーディングモードが欲しいです
TypeScript では型を書いてる分 ここにはどういうデータが入ってるのかなと思ったときにすぐにわかって便利なんだろうなと思ったことはありました
しかしある程度大きなものになると変に抽象化されていてインターフェースしかわかりません
実際にどういうオブジェクトなのか どのクラスのインスタンスなのかわかりません
クラスベースの言語ならインターフェースを実装することを明示していますが TypeScript では書かなくてもプロパティが一緒になれば同じ型として扱えるので そういう情報を見つけるのに向いていません
実際の値を見るためには breakpoint や debugger を配置して実行して値を見ます
それはもう JavaScript のときと変わりません
全然便利になった気がしません
むしろ実行しながら修正していくのは JavaScript のほうが有利です
変換処理が必要で JavaScript のようにそのまま書いて動くスクリプト言語ではなくコンパイル言語のようなものになっています
そのままで動くようにする提案もありますが JavaScript からするとそれっぽい型付け構文で間違ってるかもしれない情報が書かれるなんて迷惑でしかないです
実行時に取得したり 実行時チェックが動くならともかくコメントと同じ扱いでは入れるべきではないです
なんにせよ現状ではそのままでは使えず ESM が普通に使えるようになったのにそれらを有効利用できないイマイチなものです
React の JSX や Vue のテンプレートなど ビルド処理が必要になるツールを使うのなら ついで感覚になりますが 変換の手間を省くために htm を使ったり lit を使ったりなど JSX を避けることもあるくらいなので 足を引っ張るポイントであることに違いはありません
TypeScript はデコレータなども使えて JavaScript では未実装のものもすべて事前に含まれていると思っていましたがそうでもないようです
ECMAScript の仕様に含まれる機能は target で ES2022 など新しいものを選べば基本使えますが ブラウザの API は対応が遅いようです
数バージョン前の Chrome に実装されているのにまだ使えず自分で型定義する必要があったりします
面倒なので any にしようにも 使う場所すべてで any キャストしてからプロパティとして新機能の関数やオブジェクトを取得することになりやはり面倒です
フロント側で TypeScript を使うケースは React などで JSX も使うことは多々あります
JSX でも <> を使うので <> がどっちか判断できません
そのため JSX が有効だと型引数のあとに 「,」 を入れる変な構文で書く必要があります
また <> を使ったキャストは行えず as を使う必要があります
ジェネリクスの <> は C# などでも使われていますが これが良い記法だと思いません
D 言語だと !() ですし 別の記法を採用してる言語だってあるわけです
どうせなら別にしてほしかったです
事前に型定義されているメリットが薄いです
また名前がつけられていると Foo みたいな型名だけになってその中身がわかりません
シンプルな型なら定義に移動すればいいですが Foo は 「Bar | Baz」 みたいになっていてさらに Bar は他の組み合わせとなっていると全部の型情報を辿らないといけません
それを解決して最終的にどういうオブジェクトなのかを教えてほしいです
特に Pick とか Parameters とか Omit とか変換関数みたいな使い方をしてるジェネリクスが入ると自力で解決するのは辛すぎます
そういうのをうまく解決してくれてわかりやすく表示してくれるのがエディタや IDE の仕事だと思うのですが VSCode ではこの辺がイマイチです
こういうツールがデフォルトで整っていないのは致命的だと思います
よくこれで複雑な型定義なんかしようと思えるものですね
一応
のように map すれば展開して表示できますが union 型で {} になるなど完璧なものではないです
ジェネリクスが入った場合も 実際にどういう型とみなされてるのか分かりづらくデバッグが難しいです
普通の関数だとマウスを乗せて情報が出るところでも JSX だと表示されないことも多いです
しかし TypeScript は型で複雑なことができますし 型推論できる部分も多く 推論に推論を重ねた結果の問題点を表示されても本来の問題点とは違うことが多いです
さらに型の中身が中途半端に入って長過ぎるエラーメッセージなので読みづらいです
改行やインデントでわかりやすくしてくれるといいのですが VSCode やコンソールに出てくるエラーは長い 1 つの文字列になっていて読みづらすぎます
感覚的に プロパティや props でエラーが出ていて any にキャストしてもエラーのままなら なにか必須のプロパティが抜けていたり想定外の型で渡していて意図しない推論結果でのエラーが多いです
エラーが出ているプロパティは一旦無視して 渡すべきものが間違っていないかやすべて渡しているかを確認したほうがいいです
色々なパターンを渡せるようにすると こういうところが面倒になるんですよね
エラーメッセージに力を入れてる言語ではわかりやすくエラーが表示されているので TypeScript ではそこが重視されていないのかもしれません
エラーの情報が当てにならなすぎて 構文エラーでないなら型エラーを無視して JavaScript として動かせば その挙動や実行時エラーで原因が簡単にわかったりもするので なんのための TypeScript なのと思うことも多々あります
ですが 関数の引数は一部を除き推論されません
細かく関数を分ける書き方だとそのすべての引数に型を書かないといけなくてとても面倒です
静的型付け言語ならよくありそうなデータ型を先に定義してそれを使いまわすということも多いかと思います
しかし TypeScript はもとが動的な言語の JavaScript なのでオブジェクトの種類はとても多くなります
息をするかのようにその場限りのオブジェクトを量産します
特にポジショナルな引数を使わずオブジェクトを一つにする場合は名前の違いもあるので基本的に全部の関数で引数型が異なります
関数の数だけ型があるといってもいいかもしれません
そんななので 型の定義がとても多いです
面倒なので共通化させようとすると JavaScript のメリットが失われます
静的型付け言語としてのメリットや機能が十分にあるなら一長一短なのかもしれませんが コンパイル時に消えるなど中途半端な機能なのでデメリットのほうが多いです
型情報を利用してできることを増やせばいいのに JavaScript のスーパーセットで型情報を消せばそのまま JavaScript として動くようにする事にこだわっているので 実行時に使える便利な機能がありません
そういうメリットが多くなれば多少面倒でも型を書く気にもなりますが それがないなら JavaScript の劣化版としか思えません
組み込みでパターンマッチングとかもいいですが 単純に型情報が変数に保存されて 実行時に参照できるだけでもかなり便利になります
例えば API のリクエストや localStorage への書き込みなど出力するときには不要な情報を削除したいです
型情報があってそのキーの一覧を配列で取得できればそういうことが簡単にできます
filter みたいな便利機能は標準で用意しておいてくれてもいいくらいです
しかし現状では実行時に使える情報にはならないので JavaScript の値としてキー一覧を保持するしかありません
二重に管理してることになってすごくムダです
他にも 「Foo | Bar」 みたいな union 型で if で簡単にどっちか判断したいというときにも困ります
type プロパティを見るだけみたいな単純ならいいのですが どっちも複雑な型だと JavaScript で判定関数を書くのも面倒です
TypeScript に型情報があるのにそれを使えないのはとても不便ですし 同じことを再実装するのはムダすぎます
ブラウザで使われるので古いページを見れるようにするため変更されないことが多いです
AltJS ならそういうところをうまく改善してくれればいいのに JavaScript の基本仕様は変えないので良くないところも引き継いでます
スーパーセットで型情報を消せば動くなんてこだわりは早く捨ててほしいものです
最初は入りやすさとしてその戦略は良かったのかもですが ある程度の知名度が得られたならさっさと次のステージに進んでほしいです
型関係で言えば undefined の扱いとかどうにかしてほしいです
nullable な場合にわざわざ null | undefined なんて書くのは面倒ですし 引数やプロパティの undefined と省略可能の ? が分かれてるのも扱いづらいです
オブジェクトのプロパティでキーがあって undefined が入ってるのとキー自体がないのも合わせて undefined の扱いを見直してほしいです
明示的に undefined を入れるのってそれがプロパティとして存在することを表すためがほとんどなので型でその役割を果たせば不要になるはずです
undefined を入れればキーを消してるのと同じ扱いにしてほしいです
例えば isNaN は any 型を受け取ればいいのに number 型を受け取ることになっています
そのせいで文字列型をそのまま入れることができずキャストが必要です
any 型を受け取れる関数を用意してもいいですが TypeScript のために JavaScript 実行時に影響するようなものを用意するのは気が進みません
またキャストは JavaScript では演算子で行うことが多いのに null に + を使えないなどの制限があります
これは型エラーにされます
関数でキャストすればエラーにはなりません
関数を通さず演算子でキャストできる良さがなくなってます
他には "" と + で結合すれば toString が暗黙的に呼び出されるのに引数では許可されていません
これらはエラーにならずゆるく使えています
ですがこれはだめです
JavaScript ではよく使うコードですが location は string 型ではないのでエラーです
string になるよう location.href プロパティにしたり 明示的に toString メソッドを呼び出す必要があります
toString は全部のオブジェクトにあるので それを許可すると文字列を受け取る関数に何でも渡せてしまうというのはわかりますが null と文字列の足し算を許可するようなゆるさと合わずに微妙な気持ちになります
どっちかというとこういう関数の引数を本来の型定義どおりではなく location オブジェクトも受け取れるするようにするなど JavaScript のコードをそのまま使えるように型定義をしてほしいものです
同じく
でページ移動もできなくなっています
とても不便です
また配列の includes でも型をあわせる必要があります
配列の中に存在するかどうかなので any 型を受け取ればいいのに型が違うとエラーです
変数の型が string | number みたいな場合にわざわざ型を見て分岐するのは面倒なだけです
特にオブジェクト型だと JavaScript の処理として型チェックするのはとても面倒です
みたいな関数で XXX かどうかを判定するとします
&& は falsy な値ならそのまま返るので 返り値の型が 「boolean | null | undefined」 みたいなことになることがあります
foo に number や string が来る場合もあれば 0 や NaN や "" もありえます
そうなると boolean 型を受け取る関数にはそのまま渡せません
返り値を boolean 型に変換する必要があります
これをいちいちやっていくだけでもかなり面倒です
返り値の型を boolean と書けば自動で変換してくれればいいのに型が一致しないとエラーが出るだけです
falsy な値はすべて boolean 型を受け取るところに渡せるようにすればいいのにと思えてきます
似たもので
のようにして bar の返り値または null を返す関数のつもりでも foo に undefined や false が入ることがあれば そうはいかないです
JavaScript の基本構文を捨てないならこういうところの使い勝手はどうにかしてほしいものです
使用箇所が 1 箇所なら使用箇所から自動で型推論して欲しいです
できれば複数箇所でも同じ引数型で呼び出してるならそれで推論して欲しいですけど
よく使われる関数定義は
みたいに引数や返り値の型が関数定義の一部のように書かれると思います
この程度ならいいのですが 複雑な引数や返り値だととても読みづらく どこまでが変数名なのか型定義なのかわかりづらいです
型情報部分が複数行にもなると 引数と内部処理の位置が離れるのも嫌なところです
Elm のような言語では関数の前の行に型情報だけを書いています
そんな感じで分けて書かれていると 型情報 + JavaScript として読めて処理の部分だけを見たいときに読みやすいです
TypeScript でも
という感じで変数に型を指定してそこから推論させることができます
見やすさ的にいい感じではあるのですが この書き方では関数の返り値の型が必須になります
返り値がプリミティブ型なら別に書いてもかまわないのですが 複雑なオブジェクトや組み合わせになると書くのが面倒だし ごちゃごちゃして読みづらくなるので書きたくないです
auto とか infer を書いておけば推論に任せられるといいのですけど そういう機能はなさそうです
複雑なライブラリは複雑な型定義になってることが多いです
特に JavaScript を前提に作られてるものだと動的な考え方なので無理に静的に型付けしようとするとキレイには書けないことが多く なぜ動いてるのか理解がとても難しいものになってることが多いです
そういうのだと ドキュメントにあるような基本的な使い方のうちはいいものの 発展的なことをして使う側も複雑になってくると動きそうなものなのに型エラーが出て上手く動かないことが多いです
上でも書いたように わかりやすいエラーメッセージがあり修正できるならまだいいのですが かなり長く意味不明なエラーメッセージになることが多いです
ライブラリが動的にいろいろなパターンの入力で動くようになっているものでは いろいろな推論結果のエラーですし 長すぎるとエラーメッセージが変に省略されていますし エラーメッセージを見てどうにかするのは諦めてコードを見直すほうがいいケースも多いです
自分で書いた部分なら期待と違う方に推論された結果のエラーメッセージでもある程度は察せますが ライブラリだとそうはいかないです
JavaScript としての処理に加えて 型定義までライブラリの中身を読まないといけないのは面倒が多いです
これは使うライブラリの問題でもあるので 最初から静的型付けを想定した設計で作られていて 型定義もキレイなものだとそこまで困りません
ただ 私の場合は自分から進んで TypeScript を使わないので 基本的に自分で作ったものをいじるわけではないです
なので使われてるライブラリは自分で選んでないのでどうしようもないのですよね
自分で選ぶなら TypeScript を採用して作るなら静的な言語なんだからと割り切って 動的な考え前提で書かれているライブラリは使わないほうが苦労が減ると思います
シンプルなケースだとこういうのがあります
関数を受け取る関数 fn1, fn2 があって そこに渡す関数は型が違っても同じでいいから callback 関数を渡しています
callback 関数は両方のパターン number | string が引数でこれで問題なく動いています
問題のケースは React の onChange です
event や string や number などいろいろなケース を union 型でまとめた callback 関数相当のものを複数のイベントのリスナとして設定しようとしたらエラーでした
UI ライブラリとかも入ってきているので 変な方向に推論された結果だと思います
エラーメッセージは HTML の属性が足りないとか全く関係ない内容で HTML のタグ関係の型がどうこういうエラーでした
TypeScript ではなくライブラリの型定義の方に問題があるのかもしれないですが 現にそういうライブラリは有名どころでも普通にあります
TypeScript を使わず JavaScript ならそもそも発生しないのでこれも TypeScript の不満点です
とりあえず union 型にせず unknown や any として受け取ればエラーにはならないので 内側で型チェックすることで回避はできました
とりあえず any で対処できるかとおもいきや 逆で推論時に any だと他の型との判定ができずにエラーにされて明示的に型を書かざるを得なくなったときもありました
例えば
最後に fn に values を渡すところで string を "A" | "B" に代入できずエラーです
型を明示するか const 指定が必要になります
と書いたときに A に { foo: string } を追加して foo は重複するので後ろの string で上書きされてほしいのですが number & string は存在しないので never 型になります
は foo が never 型への代入となってエラーです
JavaScript のオブジェクトのように ... が使えるといいのですが 構文エラーでした
実現には A から foo を除外した型を作って その型と intersection で結合します
関数のように扱えるジェネリクスを作ることもできますが こういうことを始めるとどんどん複雑になっていき JavaScript では不要な苦労を味わうことになってきます
ちなみに関数だと引数の型が違っても通りました
オーバーロード用でしょうか
こういう感じのコードを書きたいことは多いのですが B の場合は a プロパティがないからエラーです
B はオブジェクトなので null のプロパティ読み出しみたいにエラーにはならないのに許可されません
in を使って
にしないといけないのですが JavaScript で in はほぼ使わないものなのであまり使いたくないです
それにこういうケースでは a プロパティがある A のパターンでも空文字など falsy なら if に入る必要がないというケースが多いです
in ではそのチェックはできないので a があるかチェックしてさらに falsy でないチェックをしないといけません
2 段階になって手間が増えすぎです
type の省略時は foo なので扱いやすいようにデフォルト引数で foo を入れます
こうすると type が bar のときに value が string と判断してくれません
number | string の union 型になって string のメソッドが使えません
型を再定義すればできなくもないですが すごく手間ですし TypeScript の都合で読みづらくなります
result は (string | number)[] という型になるので push はできますが string[] または number[] ではないので fn2 に渡せません
if ブロックの中で代入すればそれぞれの型になりますが冗長です
こういうケースもあります
list と callback は同じ型ですがエラーになります
にすればエラーは回避できますが さっき以上にムダな感じがします
このケースだと type は使わないですし
と引数型を変更すれば 処理部分はシンプルにできますが 実際にはもう少し関数の本体部分が長くてこういう風にできないことも多いのですよね
期待する結果は分解した状態でそれぞれから指定のプロパティを除いたものです
union 型はすべてに含まれるプロパティだけ取り出した上で Omit 対象を除外したような動きになるようです
昔ながらの enum 型に近いのだとこういう使い方でしょうか
Answer は answer の value の union 型なので 1 を直接渡せてしまいます
それだとミスで別の値を渡してもちょうど 1 などの数字なら通ってしまいます
そういう意味ではキーの文字列のほうがいいのかもしれません
value や callback の型をちゃんと書いてないのでこれにちゃんとした型を指定します
これだと T が { x: number } と関係ないところでインスタンス化されるかもというエラーになりました
T に { x: number } が含まれる制限をつけるために T の条件として { x: number } を extend するように指定しましたがそれでも T のサブタイプかもみたいなエラーです
結局 引数として渡す値を as で T とみなすことで動かせました
ただの関数だと簡単な対策で済みましたが React の props だと面倒になりますし 視認性も落ちます
上のケースでさらに callback の引数が x のみで value が空になるなら省略可で そうじゃない場合は省略不可にしたいです
そうなってくると型の条件分岐が必要になって複雑です
型定義不要の JavaScript の fn 関数と比べるとすごく長く読みづらいです
JavaScript の上の関数をみるだけで value と callback の関係は十分わかります
そのときに as でキャストが必要で面倒です
手抜きで any としても string や number はキーとして不正なのでエラーになります
any なら何でも通してくれていいのですけど
langs の方を any にすれば通りますが () が入って見づらくなります
JSX でコンポーネントとして使う関数は返り値が Element | null である必要があるらしく undefined の可能性のある ReactNode だとエラーです
この回避のために children を <></> でラップしないといけません
JavaScript では不要な手間が必要になります
これは TypeScript 自身ではなく React ライブラリの型定義の問題ですね
このときの convert 関数の定義で value の型が必要になります
複雑な処理の結果生まれるものなので 静的に定義するのは難しく 変わりやすい部分なので引数に変更があるたびに型定義の変更は面倒です
fn の中なら typeof value で型を取得できるので容易ですが 型以外に fn の中の値を参照しないなら convert 関数は外側のスコープに持っていきたいです
complexFn 関数の呼び出し部分だけを取り出して
みたいに型情報取得のための関数を用意してそこから型情報を取得することはできます
ですが TypeScript の都合でムダな関数を作るわけですし スッキリとしない方法です
自分で引数の複雑な組み合わせを解決しなくても同じ式をコピペするだけで TypeScript がやってくれるのですが 二重に書かないといけないことに変わりはありません
また 上の例の a や b も fn が引数として受け取る値をもとに関数を通して決まる型ということも少なくないです
事前に型定義ありきで作られたものではなく 必要なものを全部詰め込んで動的に作っていくようなものと相性が悪いです
value1 と value2 は [string, number][] という型です
それに対して value3 は [string, unknown][] です
ジェネリクスの通り [T, number][] となって欲しいのですけど
直接的な関数はないので Object.entries で配列化してから map したあとに Object.fromEntries でオブジェクトにします
しかしこのときに TypeScript だと型情報が思い通りになりません
これは a2 への代入でエラーになります
右辺の型は { [k: string]: number } となっているので x や y があるか判断できないためです
JavaScript 的にはよくある処理なのにこれをやるだけで型で上手く動かないのはかなり使いづらいです
as でキャストすれば通せますが 安全ではないです
こういうのでも通せてしまいます
このときの a2 に x はないので a2.x.toFixed() みたいなことをすると型エラーは出ないのに実行時エラーが起きるコードになります
あとから処理を書き換えたときに as が問題ないかもチェックが必要になってきます
としたときの entries の型が
[string, string | number | boolean][]
となっています
期待する型はこういうのです
そうなるような objectEntries を自作します
k を a や b で絞り込むと v の型が正しく判断されてメソッドが使えています
ただこれを map するともとのような形で残りませんし fromEntries でキレイにオブジェクト型に戻りません
という型の関数があります
結果を分割代入すると value は unknown になります
value は number なので number にしたいのですが value に代入された時点で value の型が unknown に決まってしまうので扱いづらいです
のように仮の変数を作ってからキャストになります
TypeScript の都合で JavaScript では不要な処理です
変数を作らない場合は代入前のキャストですが 一部だけをキャストできません
のように name も必要です
1 つくらいならまあいいのかもですけどプロパティが多い場合にはやってられません
本来の fn の返り値の型から value を除外した型と { value: number } の型を & でつなげばマシですが 面倒なことに変わりありません
1 つめの引数の型をベースに 2 つめの型で上書きした型を返す Override ジェネリクスを用意して書いても↓です
一旦 unknown で受け取ってから別変数にキャストして入れたほうが見やすいと思います
形式でプロパティを書こうとすると prop が候補に出てきて 型情報も取得できます
しかし その型情報を取得して別名にしようとするとエラーになります
で取得できません
実際に確認可能なものは MUI の SxProps で確認できます
sx のオブジェクトの中身を書くときに display を補完で出せますし flex や block などは選択肢から選べます
なので SxProps に display プロパティがあると認識されてそうなのですが 下側のように Display に代入しようとすると display プロパティはないというエラーになります
npm ライブラリの import があっても TS Playground で確認できました
TS Playgound
ジェネリクス関係な気もしてますが どちらも推論できるだけの情報がないはずなんですよね
デフォルト値ならどっちも同じになりそうですけど
1 つはソースコードが切り替わったのに型チェックではうまく新しい情報に切り替わっていないようで 正しいはずのところでエラーになります
Git 以外でもツールが自動でコードを生成したときなどにも起きます
CRA とか Vite などの監視して更新するツール側でもそこそこありますが VSCode ではより頻繁に起きます
待ってても改善しないことがほとんどなので TS Server の再起動で対処してます
もう一つは必要ないところでも型がチェックされることです
開発中にあれこれ試すためのコードをバージョン管理外のファイルで用意してることが多いです
新機能を作ってるブランチで バージョン管理外のコードから新機能のモジュールや関数を参照します
その状態で今の main ブランチでの動作を確認したいなと思って切り替えることが結構あります
バージョン管理外のコードはブランチ切り替えに影響はしないですが 切り替え後には新機能のモジュールや関数がないので静的解析でエラーになってしまいます
バージョン管理外のコードの中身を一旦どこかに退避してから 全部消してやっと動くようになります
確認後にはブランチを戻して バージョン管理外のコードをもとに戻してとすごく手間になります
value1 と value2 は型が同じである必要があります
同じであれば何でもいいです
これを配列化するとき T は配列の要素ごとに異なる可能性があります
1 つめは number のペアで 2 つめは string のペアなどです
しかし T が 「number | string」 となると 1 つの Foo の中で value1 が string で value2 が number というのも許可されてしまいます
実現不可能ではないらしいですが 単純な方法ではできず チェック用の関数を通す必要があったり 手間が多くやりたい方法ではないです
TypeScript の都合で Foo のオブジェクトを作るたびに余計な関数を通すことになりますし
実際に これそのままのケースだとあまりないですが プロパティの 1 つが関数で その引数の型がもう一方のプロパティというケースはときどきあります
React だと Component と props のペアをまとめて保持するときにこうなります
なのにそれを簡単に解決できないのは言語機能が十分でないと思うんです
in は undefined でもキーがあれば true になるので undefined の扱いがさらに面倒になります
ほとんどの場合 キーがあって undefined か キーがないかは気にしなくていいいです
プロパティにアクセスして undefined かどうかをみればいいです
キーがあってもなくても undefined が得られます
なのでプロパティを消したいときに delete 演算子は使わず undefined を代入します
V8 だとキーの追加や削除より上書きするほうがパフォーマンスに優れるとも聞きますし
しかし TypeScript はこれができないので in にする必要があり in だと undefined を入れても true になるので delete を使わないといけなくなります
delete なんて with と同じくらい使わないものなので それを使わざるを得なくなるのが嫌なところです
実際に困る例はこういうのです
t では B の方の型として扱われることを期待するのに A として扱われてしまっています
と書ければ問題ないのですけどね
やりたいことは直接的に
です
複雑な型になってくると自分で判定する関数を用意したくないので 内部的に判断できてるなら TypeScript に任せたいものです
ですが 実行時に型情報がないのでそういうことができません
せめて判定する関数を自動で生成して is キーワードを使ったらその関数を呼び出して判定するような変換くらいはしてほしいものです
引数は型情報だけでよく
だけで良いはずです
それにここでいう B の型定義は使い回すことがあり number 型が長さを指すこともあれば回数を指すこともありえます
型定義の時点で名前は決まらず とりあえず arg1 とか意味のない名前になることも少なくないです
書くことができるのはいいですが 必須にされても面倒なだけです
ただ面倒なだけでなくネストするととても見づらいです
名前が不要ならもっとスッキリします
型定義を別に書くならあまり気になりません
しかし 直接引数のところに書くと
オブジェクト全体の型として分けて書かないといけないです
通常の引数で
と書けるように各プロパティの横に書きたいです
しかし この構文の : は JavaScript として意味があるものなので別の意味として解釈されます
JavaScript に継ぎ足ししたような言語にせず 型付きの言語として 1 から構文を考えて作ってほしかったです
現状の書き方ではオブジェクトのプロパティが多いと変数と型情報が離れすぎていて分かりづらくなります
しかし JavaScript で使われる実体と統合されないので 型 T のデフォルト値を取得ということができません
これは param.foo のアクセスがエラーになります
プロパティが存在するチェックをしてるのだから any 型ででも受け取れればいいのにできません
foo を持つ型を用意してそれへのキャストが必要になります
見づらく面倒です
またこの場合は A という一文字になっているのでまだマシで param: のあとに直接オブジェクト定義を書いているともっと面倒になります
みたいにして 値が無いならまとめて 0 として扱いたいケースは多々あるのですけどね
ライブラリなどで不親切な型定義だと X があるなら Y もあるはずみたいなのを適切に表現してないので個別に対処しないといけないことあってとても面倒です
型的にそれをやろうとするととても面倒です
静的にわかるものなら Omit が使えますが 引数で渡されるキーを除外したい場合に困ります
引数がリテラルなら T として受け取れるのですが 配列に入っていて string という情報だけだとたぶんどうしようもないです
このときの exclude 関数をうまく定義できないです
インターフェースを変えてキーの配列ではなく 除外したいキーをキーとして持つオブジェクトならマシかもしれません
使いやすさはイマイチで無意味な value も書く必要があるのですが オブジェクトなら配列と違ってキーは保持されるので 除外すべきキー情報を引数の型から特定できます
ただ完全に TypeScript の都合なので それのために JavaScript としての処理を変えるのはあまりやりたいとは思えません
foo の場合は即 return してるので返り値は bar または undefined です
返り値の型でこれが正しく推論できています
最後の処理で関数を使って処理します
消えたはずの foo が復活しています
関数内の arg の時点では x が "foo" | "bar" に戻っています
その場で作って実行する即時関数なので直前の状態を反映して欲しいですがそういうことはできないようです
obj ごと渡さず obj.x だけなら絞り込み状態の型になるみたいです
絞り込まれてるのは obj.x の型であって obj 型自体は変わらずということみたいです
オブジェクトのプロパティなら変数と違って別のどこかで書き換えられて絞り込んだのが意味なくなることがあるから?とか考えても見ましたがやろうとすると現状の絞り込みでも実行時エラーを起こせます
このオブジェクトの x プロパティは読み取るたびに foo と bar が切り替わります
これをさっきの関数に入れると
のようになって推論された "bar" | undefined 以外の値が返ってきてます
なので特別な意味があるというよりはそこまでサポートしてなくて ただ不便なだけだと思います
f1 と f2 は a が string と確定するので slice メソッドの呼び出しが可能です
f3 も f2 と同じかと思いきや if 文が実行されないケースがあると判定されて a.slice() でエラーです
x は boolean 型となって true として判断してくれません
絶対来ないのに TypeScript のために if 文が必要です
もっとよくあるのは外側スコープの変数を参照するときに 関数が呼び出されるなら nullable ではないというパターンです
inner 関数を呼び出す側で value が null にはならないときのみ呼び出すのですが それを TypeScript は理解できないので inner の中で value のチェックをしないといけません
UI を作るときに disabled を使って制限してることはけっこうあります
nullable なら value!.slice() のように ! をつけることができて少し楽です
しかし他の型だと自分で判定して if 文を作るか as でキャストする必要があります
ウェブサーバーで言えば 先にリクエストのデータのバリデーション処理やデータベースから取得や保存の処理を書いて 最後にそれらを使ったリクエストハンドラの処理を書くのが前者です
先にリクエストハンドラでやること全体を書いて その中のデータベースに送るクエリなどを後で書くのが後者です
TypeScript など静的型付け言語だとボトムアップの方法が向いています
すでに用意してるパーツなら補完と相性がよく インポート文や関数名などで補完が使えます
それに対してトップダウンだと存在しない関数やモジュールが存在する前提で書いて 後からそれを書くので補完はできません
それどころか存在する中から似ている名前のもので無理やり補完されてしまうなどもあって迷惑です
知らない間によくわからないモジュールのインポートがファイルの最初に追加されていたなんてこともあります
また 存在しない関数や型を書くとエラーを表す赤色の線が画面に出ます
最初だとほとんどの関数は存在しない状態なので 赤線だらけで気持ち悪いです
まだ作っていないものがわかるので便利といえば便利ですが わかりやすくするために変に目立つのでずっと出てると不快です
あのエラー通知はチェックするボタンを押したときだけ出るようにして欲しいくらいです
自分の場合は先に全体の処理をなんとなく書いてから細かいところを実装していくことが多いので 上記の通り相性が悪いです
変数の型を取得できるのですが その時点でありえる型に制限されています
foo が 「Foo | null」 という型で foo が null のときのデフォルト値を as で Foo 型にしようとしたらうまく動きませんでした
?? の右側なので null か undefined です
この場合の型だと null になります
「as null」 となるのでだいたいの場合はここでキャストエラーです
期待してるのは Foo 型になることですがそうなってくれません
のようにしないといけないです
Foo が型として定義されているならそれを使えばいいのですが 簡単に書ける形になっていなくて typeof のほうが早いケースはけっこうあります
条件演算子で分岐して プロパティ p を持つオブジェクトを作っています
この結果を変数 u に代入して p のプロパティを取得するのと 分割代入で直接 p を取得するので型が変わります
一旦 u に入れると意図する 「XY[] | undefined」 という型で取れますが 分割代入だと string[] という型が入ってきます
s の型を XY[] から XY にして配列じゃなくしてみます
すると
どちらも同じ結果で string 型は入ってきません
単体だと
分割代入は string[] になってしまってます
オブジェクト化を通さなければ
XY[] になります
わけがわかりません
ここでまた配列ではなくしてみます
string にはなりません
どういう処理をしてるのかわかりませんが 分割代入は一旦オブジェクトに入れてそれを取り出す挙動のはずなので 型が変わるのはおかしいと思うんです
return "bar" は条件が false なので絶対実行されません
また switch は 1 で値が固定なので 2 や default のケースに来ることもありません
switch で全パターンが return してるので最後の return "baz" も実行されません
到達しないコードは静的解析で Unreachable code としてエラーが表示されます
ただ default ケースではエラーにならないようです
実際はもっと複雑なコードで default があってもエラーにならないので 来る可能性があるのだろうなと思っていたものの どういうパターンがあるかを考えても思いつかず悩んでいたのですが TypeScript がエラーとして扱わないみたいでした
default ケースに来ることがないなら switch に渡した変数の型を default ケースの中で見ると never になっています
確認のために一旦変数に入れないといけないのは少し面倒でもあります
ちなみに 絶対入らない default ケースの中でも return するとその型が関数の返り値の型として union 型に含まれます
だと A 型を渡して D 型が返ってくることもある型となり ありえない型が入ることによって使う側が面倒になります
別々の関数定義に分けて & すれば表現できるのですが 作るときに as が必要になってイマイチです
関数の as は関数部分をカッコで囲まないといけないですし 書きやすさ見やすさ的に他関数より劣ってると思います
代入先に型情報を持たせる事も考えましたが エラーでした
ただ 逆もあって TypeScript バージョンを上げるとこれまで問題なかったところで型エラーが起きることがあります
緩かった部分がより厳密になったとか デフォルトの設定値が変わったとかなんでしょう
最近だと 4.8 系に上げると多数の型エラーが起きたものがありました
特に外部ライブラリが関係するようなところでムリヤリ型を合わせてるようなことをしてたところだとかなりつらいです
TypeScript 的にエラーが出てないしこれでいいやと結構適当にやっていたのでしょう
自分でも JavaScript で書いてエラーが出なくする程度に型の補足を書く程度に使っているのでこういう問題が出ることはありそうです
ただ JavaScript 的には問題なく動くコードなのに TypeScript の都合でバージョンを上げて色々修正したりしないといけなくて時間をかけるのってすごくムダなことをしてる気持ちになります
else の場合はないことも多く JSX で
と書くような感じで
と書くことも多いです
undefined や null や false などでは ... では何も展開されないので falsy な値で問題は起きません
スッキリかけて気に入ってる書き方なのですが TypeScript では型が違うとエラーにされます
配列だと null を ... で展開しようとすると JavaScript の実行時エラーなので わかりますが null を許容しているオブジェクトでまでエラーにするのは TypeScript の型定義がおかしいとしか思えません
そのせいで冗長なこんな書き方になります
items にはもっと多くの要素があるとして find で str がある最初の A を取得します
その A の中にある str を取得したいです
find で見つかった時点で obj_or_undef が undefined でないことは確実です
なのでこう書きたいです
しかし TypeScript のエラーになるので ?. にしないといけません
?. があると null や undefined が来ることがある場所だと思ってしまうので null や undefined が確実に来ない場所で使うのは避けたいです
!. にもできますが余分な一文字があるのは同じですし チェックせず強制するものなので find の部分が変わったら問題が出る可能性があり積極的に使いたくないです
ちゃんと対処するなら find 関数が型をチェックするようにしてこうする必要があります
流石に面倒で find の度にこんなのを書きたくはないです
エラーの内容はこういうものでした
Each member of the union type '...' has signatures, but none of those signatures are compatible with each other.
find の方は異なる型の配列の union だとだめらしいです
map で動くなら find で動いてもいいと思うのですがエラーになります
items の型を配列同士の union 型から union 型の配列に変えると動くようです
配列同士の union の map で 値をそのまま返す value1 の結果が union 型の配列になるので
のようにすればエラーなく動かせますが ムダな処理が増えてます
そのまま find したときに 引数が A | B になって 返り値が A | B | undefined でいいと思うのですけどね
T は baz だけのオブジェクトになってほしいけど foo, bar, baz 全部を含んだオブジェクトになる
全部含む型で受け取って Omit するしかなさそう
型定義時に 色々なジェネリクスが入って同じものを複数箇所で使う場合は一時変数に入れたくなります
だと
みたいなことがしたいですが これができません
同じ型でも毎回書く必要があって長い型になるととてもつらいです
Excel の数式を書いてる気分になります
Excel でも最近は LET 関数があるのでもっとマシです
一応できなくもない方法はあるのですが 良い方法とは言えないです
追加の型パラメーターのデフォルト値を使います
動きはしますが 3 つ目を渡すことができて 渡すとこうなります
1, 2 番目の型パラメーターが完全に無視されます
関数で言えば
を
と書くようなものです
できると言っても積極的にやりたいものじゃないです
やるにしても 書き方を工夫して 後半が一時変数の特殊な型パラメータであるとわかりやすくしておきたいものです
追加があればしばらくの間はこの記事を追記するかもしれません
しかし 最近では TypeScript を使って書かれているものが増えてるので それらをいじるときなど 仕方なく触らざるを得ない機会も増えています
不満点を感じるたびにメモに書いていたら 1000 行近くになっていたので記事にまとめてみることにしました
一部は再確認のために最新版で試すと改善されてるのもあったので改善済みのもあるかもしれません
遅い
当たり前ですが 型チェックというフェーズが増えますし JavaScript への変換処理も入ります実行時は JavaScript なのでその速度は基本は変わりませんが JavaScript にするまでの処理や初回の準備などがあるので開発の速度は遅くなります
また一部は TypeScript の都合で不要な処理を追加する必要があり それはそのまま JavaScript になるため JavaScript のムダな処理が増えます
ほとんどは誤差程度ですが JavaScript よりは遅いことに変わりありません
一番の問題の型チェック部分の速度ですが かなり遅いです
ゲーム用途ならともかく 一般用途としてはそれなりにハイスペックの PC でも目に見えて遅いです
一番適してそうな VSCode を使っているのに JavaScript に比べてかなりの遅さです
構文のエラーや型エラーで赤色の線が出たりしますが それが出たり消えたりするのに何秒も待たないといけません
ひどいときは何分待っても反応しないのでコマンドから TS Server の再起動をして解決したりするほどです
ただ常にそんなに遅いかというとそうでもなく ちょっと動作確認のために数十行のコードを書くとかならほぼ一瞬で JavaScript を書いてるときと違いを感じません
ですが ライブラリを色々入れて大きめのものになってくるととても遅くなってきます
標準的な TypeScript ツールが JavaScript で動いていてシングルスレッドだからでしょうか
それに加え できる限り型は書きたくないので 積極的に推論に任せているのでさらに遅くなっているのかもですね
TypeScript コンパイラを Go や Rust で作るような話も見かけたのでそれらによってはマシになるかもしれません
読み書きに時間がかかる
上では変換や解析の時間について書きましたが そもそもコードを読み書きする時間も TypeScript のほうがかかりますJavaScript で十分な人からすれば余計な型情報を書かないといけないです
見ればこういうことだろうと人なら分かる部分でも 機械的に解析できないといけないのでコンパイラのためにあれこれ工夫しないといけないところが出てきます
私が静的型付けが嫌いかつ慣れてもないという部分もありますが JavaScript でコードを書いてから TypeScript コンパイラを通すために型情報を追加で書いていると ある程度複雑なところになると JavaScript でコードを書いたのとほぼ同じくらいの時間がかかってます
シンプルな計算のコードで型も単純なものならともかく React で UI を作るみたいに複雑なのになってくると 型定義も複雑になるので JavaScript のコードを書いてる時間以上に型エラーにならずにコンパイルできるようにするのに時間を使ってる気がします
コードが見づらい
↑とも関連しますが JavaScript で十分な人からすれば TypeScript で追加される情報は基本どれも不要です複雑なジェネリクスや & や | で型を組み合わせたものが入ってくると型定義だけで何行にもなり 処理が見づらいです
行が分かれていればまだマシで JavaScript としては不要な as キャストなども入っていると見やすさは最悪です
C# や Java みたいなごちゃごちゃしたコードになってきます
読むだけなら型情報はなくても全く困りません
動いてるコードなんだから .foo プロパティにアクセスしていれば .foo があるのはわかります
多くの場合 それの具体的な型がなんであるかは知る必要がないです
型情報を消して JavaScript として表示して マウスオーバーで型が表示されるようなリーディングモードが欲しいです
具体的な型はわからない
コードを修正したりするときにはここにどういう値が入ってるかを知ることは重要になりますTypeScript では型を書いてる分 ここにはどういうデータが入ってるのかなと思ったときにすぐにわかって便利なんだろうなと思ったことはありました
しかしある程度大きなものになると変に抽象化されていてインターフェースしかわかりません
実際にどういうオブジェクトなのか どのクラスのインスタンスなのかわかりません
クラスベースの言語ならインターフェースを実装することを明示していますが TypeScript では書かなくてもプロパティが一緒になれば同じ型として扱えるので そういう情報を見つけるのに向いていません
実際の値を見るためには breakpoint や debugger を配置して実行して値を見ます
それはもう JavaScript のときと変わりません
全然便利になった気がしません
むしろ実行しながら修正していくのは JavaScript のほうが有利です
AltJS
あくまで AltJS のひとつです変換処理が必要で JavaScript のようにそのまま書いて動くスクリプト言語ではなくコンパイル言語のようなものになっています
そのままで動くようにする提案もありますが JavaScript からするとそれっぽい型付け構文で間違ってるかもしれない情報が書かれるなんて迷惑でしかないです
実行時に取得したり 実行時チェックが動くならともかくコメントと同じ扱いでは入れるべきではないです
なんにせよ現状ではそのままでは使えず ESM が普通に使えるようになったのにそれらを有効利用できないイマイチなものです
React の JSX や Vue のテンプレートなど ビルド処理が必要になるツールを使うのなら ついで感覚になりますが 変換の手間を省くために htm を使ったり lit を使ったりなど JSX を避けることもあるくらいなので 足を引っ張るポイントであることに違いはありません
新機能への対応が遅い
Webpack などと同じように Chrome では動く JavaScript でも TypeScript が対応していないせいで使えない機能がありますTypeScript はデコレータなども使えて JavaScript では未実装のものもすべて事前に含まれていると思っていましたがそうでもないようです
ECMAScript の仕様に含まれる機能は target で ES2022 など新しいものを選べば基本使えますが ブラウザの API は対応が遅いようです
数バージョン前の Chrome に実装されているのにまだ使えず自分で型定義する必要があったりします
面倒なので any にしようにも 使う場所すべてで any キャストしてからプロパティとして新機能の関数やオブジェクトを取得することになりやはり面倒です
JSX との相性
TypeScript はジェネリクスが多用されますが ジェネリクスを表す記号は <> ですフロント側で TypeScript を使うケースは React などで JSX も使うことは多々あります
JSX でも <> を使うので <> がどっちか判断できません
そのため JSX が有効だと型引数のあとに 「,」 を入れる変な構文で書く必要があります
const a = <T,>(arg: T) => arg
また <> を使ったキャストは行えず as を使う必要があります
ジェネリクスの <> は C# などでも使われていますが これが良い記法だと思いません
D 言語だと !() ですし 別の記法を採用してる言語だってあるわけです
どうせなら別にしてほしかったです
型の詳細が見えない
VSCode でマウスを乗せて型情報を表示しても長い型は ... で省略されて中身が見えないです事前に型定義されているメリットが薄いです
また名前がつけられていると Foo みたいな型名だけになってその中身がわかりません
シンプルな型なら定義に移動すればいいですが Foo は 「Bar | Baz」 みたいになっていてさらに Bar は他の組み合わせとなっていると全部の型情報を辿らないといけません
それを解決して最終的にどういうオブジェクトなのかを教えてほしいです
特に Pick とか Parameters とか Omit とか変換関数みたいな使い方をしてるジェネリクスが入ると自力で解決するのは辛すぎます
そういうのをうまく解決してくれてわかりやすく表示してくれるのがエディタや IDE の仕事だと思うのですが VSCode ではこの辺がイマイチです
こういうツールがデフォルトで整っていないのは致命的だと思います
よくこれで複雑な型定義なんかしようと思えるものですね
一応
type Debug = { [key in keyof T]: T[key] }
のように map すれば展開して表示できますが union 型で {} になるなど完璧なものではないです
ジェネリクスが入った場合も 実際にどういう型とみなされてるのか分かりづらくデバッグが難しいです
普通の関数だとマウスを乗せて情報が出るところでも JSX だと表示されないことも多いです
エラーメッセージがあまり当てにならない
シンプルな型定義なら分かりやすいエラーでどう悪いのかがわかりますしかし TypeScript は型で複雑なことができますし 型推論できる部分も多く 推論に推論を重ねた結果の問題点を表示されても本来の問題点とは違うことが多いです
さらに型の中身が中途半端に入って長過ぎるエラーメッセージなので読みづらいです
改行やインデントでわかりやすくしてくれるといいのですが VSCode やコンソールに出てくるエラーは長い 1 つの文字列になっていて読みづらすぎます
感覚的に プロパティや props でエラーが出ていて any にキャストしてもエラーのままなら なにか必須のプロパティが抜けていたり想定外の型で渡していて意図しない推論結果でのエラーが多いです
エラーが出ているプロパティは一旦無視して 渡すべきものが間違っていないかやすべて渡しているかを確認したほうがいいです
色々なパターンを渡せるようにすると こういうところが面倒になるんですよね
エラーメッセージに力を入れてる言語ではわかりやすくエラーが表示されているので TypeScript ではそこが重視されていないのかもしれません
エラーの情報が当てにならなすぎて 構文エラーでないなら型エラーを無視して JavaScript として動かせば その挙動や実行時エラーで原因が簡単にわかったりもするので なんのための TypeScript なのと思うことも多々あります
多くの型を書かないといけない
関数の返り値や代入は右辺から推論できるので 一見すると型を書く必要はあまりないように見えますですが 関数の引数は一部を除き推論されません
細かく関数を分ける書き方だとそのすべての引数に型を書かないといけなくてとても面倒です
静的型付け言語ならよくありそうなデータ型を先に定義してそれを使いまわすということも多いかと思います
しかし TypeScript はもとが動的な言語の JavaScript なのでオブジェクトの種類はとても多くなります
息をするかのようにその場限りのオブジェクトを量産します
特にポジショナルな引数を使わずオブジェクトを一つにする場合は名前の違いもあるので基本的に全部の関数で引数型が異なります
関数の数だけ型があるといってもいいかもしれません
そんななので 型の定義がとても多いです
面倒なので共通化させようとすると JavaScript のメリットが失われます
静的型付け言語としてのメリットや機能が十分にあるなら一長一短なのかもしれませんが コンパイル時に消えるなど中途半端な機能なのでデメリットのほうが多いです
型情報は実行時に残らない
TypeScript ではせっかく型情報を書いてもコンパイル時のチェックしかしません型情報を利用してできることを増やせばいいのに JavaScript のスーパーセットで型情報を消せばそのまま JavaScript として動くようにする事にこだわっているので 実行時に使える便利な機能がありません
そういうメリットが多くなれば多少面倒でも型を書く気にもなりますが それがないなら JavaScript の劣化版としか思えません
組み込みでパターンマッチングとかもいいですが 単純に型情報が変数に保存されて 実行時に参照できるだけでもかなり便利になります
例えば API のリクエストや localStorage への書き込みなど出力するときには不要な情報を削除したいです
型情報があってそのキーの一覧を配列で取得できればそういうことが簡単にできます
filter みたいな便利機能は標準で用意しておいてくれてもいいくらいです
type A = {
foo: string,
bar: number,
}
const obj = {
foo: "",
bar: 1,
baz: true,
}
A.filter(obj)
// { foo: "", bar: 1 }
しかし現状では実行時に使える情報にはならないので JavaScript の値としてキー一覧を保持するしかありません
二重に管理してることになってすごくムダです
他にも 「Foo | Bar」 みたいな union 型で if で簡単にどっちか判断したいというときにも困ります
type プロパティを見るだけみたいな単純ならいいのですが どっちも複雑な型だと JavaScript で判定関数を書くのも面倒です
TypeScript に型情報があるのにそれを使えないのはとても不便ですし 同じことを再実装するのはムダすぎます
JavaScript の不便なところを改善しない
JavaScript は歴史的な経緯で微妙な部分は色々と残っていますブラウザで使われるので古いページを見れるようにするため変更されないことが多いです
AltJS ならそういうところをうまく改善してくれればいいのに JavaScript の基本仕様は変えないので良くないところも引き継いでます
スーパーセットで型情報を消せば動くなんてこだわりは早く捨ててほしいものです
最初は入りやすさとしてその戦略は良かったのかもですが ある程度の知名度が得られたならさっさと次のステージに進んでほしいです
型関係で言えば undefined の扱いとかどうにかしてほしいです
nullable な場合にわざわざ null | undefined なんて書くのは面倒ですし 引数やプロパティの undefined と省略可能の ? が分かれてるのも扱いづらいです
オブジェクトのプロパティでキーがあって undefined が入ってるのとキー自体がないのも合わせて undefined の扱いを見直してほしいです
明示的に undefined を入れるのってそれがプロパティとして存在することを表すためがほとんどなので型でその役割を果たせば不要になるはずです
undefined を入れればキーを消してるのと同じ扱いにしてほしいです
組み込み型の型が JavaScript の使用方法とあってない
組み込み関数の引数の型などが必要以上に厳密で JavaScript では不要だったキャストが要求されます例えば isNaN は any 型を受け取ればいいのに number 型を受け取ることになっています
そのせいで文字列型をそのまま入れることができずキャストが必要です
any 型を受け取れる関数を用意してもいいですが TypeScript のために JavaScript 実行時に影響するようなものを用意するのは気が進みません
const isNaN: (value: any) => boolean =
value => window.isNaN(value)
またキャストは JavaScript では演算子で行うことが多いのに null に + を使えないなどの制限があります
const value: string | null = getValue()
console.log(+value)
これは型エラーにされます
console.log(Number(value))
関数でキャストすればエラーにはなりません
関数を通さず演算子でキャストできる良さがなくなってます
他には "" と + で結合すれば toString が暗黙的に呼び出されるのに引数では許可されていません
"" + null
"" + 1
これらはエラーにならずゆるく使えています
ですがこれはだめです
new URL(location)
JavaScript ではよく使うコードですが location は string 型ではないのでエラーです
string になるよう location.href プロパティにしたり 明示的に toString メソッドを呼び出す必要があります
toString は全部のオブジェクトにあるので それを許可すると文字列を受け取る関数に何でも渡せてしまうというのはわかりますが null と文字列の足し算を許可するようなゆるさと合わずに微妙な気持ちになります
どっちかというとこういう関数の引数を本来の型定義どおりではなく location オブジェクトも受け取れるするようにするなど JavaScript のコードをそのまま使えるように型定義をしてほしいものです
同じく
location = "/"
でページ移動もできなくなっています
とても不便です
また配列の includes でも型をあわせる必要があります
[1, 2].includes("foo")
配列の中に存在するかどうかなので any 型を受け取ればいいのに型が違うとエラーです
変数の型が string | number みたいな場合にわざわざ型を見て分岐するのは面倒なだけです
特にオブジェクト型だと JavaScript の処理として型チェックするのはとても面倒です
falsy な型
boolean 型として扱いたいものを正確に boolean 型にしないといけないのも面倒なところですconst isXXX = () => {
return foo && foo.bar === baz
}
みたいな関数で XXX かどうかを判定するとします
&& は falsy な値ならそのまま返るので 返り値の型が 「boolean | null | undefined」 みたいなことになることがあります
foo に number や string が来る場合もあれば 0 や NaN や "" もありえます
そうなると boolean 型を受け取る関数にはそのまま渡せません
返り値を boolean 型に変換する必要があります
const isXXX = () => {
return !!(foo && foo.bar === baz)
}
これをいちいちやっていくだけでもかなり面倒です
返り値の型を boolean と書けば自動で変換してくれればいいのに型が一致しないとエラーが出るだけです
falsy な値はすべて boolean 型を受け取るところに渡せるようにすればいいのにと思えてきます
似たもので
const fn = () => {
return foo && bar(foo)
}
のようにして bar の返り値または null を返す関数のつもりでも foo に undefined や false が入ることがあれば そうはいかないです
JavaScript の基本構文を捨てないならこういうところの使い勝手はどうにかしてほしいものです
関数の引数の型推論
ライブラリみたいに使ってもらう前提のところなら引数を明示的に指定するのでいいですが 処理を分けるためだけに関数分割するなど 使う所ありきでそれに合わせて作る関数だといちいち引数の型を書くのは面倒です使用箇所が 1 箇所なら使用箇所から自動で型推論して欲しいです
できれば複数箇所でも同じ引数型で呼び出してるならそれで推論して欲しいですけど
関数の型宣言
これはとても個人的なことですが 型の情報は JavaScript との処理と分けて書きたいですよく使われる関数定義は
const fn = (a: number, b: { foo: string }): boolean => {
return b.foo.length === a
}
みたいに引数や返り値の型が関数定義の一部のように書かれると思います
この程度ならいいのですが 複雑な引数や返り値だととても読みづらく どこまでが変数名なのか型定義なのかわかりづらいです
型情報部分が複数行にもなると 引数と内部処理の位置が離れるのも嫌なところです
Elm のような言語では関数の前の行に型情報だけを書いています
そんな感じで分けて書かれていると 型情報 + JavaScript として読めて処理の部分だけを見たいときに読みやすいです
TypeScript でも
const fn: (a: number, b: { foo: string }) => boolean =
(a, b) => {
return b.foo.length === a
}
という感じで変数に型を指定してそこから推論させることができます
見やすさ的にいい感じではあるのですが この書き方では関数の返り値の型が必須になります
返り値がプリミティブ型なら別に書いてもかまわないのですが 複雑なオブジェクトや組み合わせになると書くのが面倒だし ごちゃごちゃして読みづらくなるので書きたくないです
auto とか infer を書いておけば推論に任せられるといいのですけど そういう機能はなさそうです
ライブラリとの相性
TypeScript の型定義は複雑なことができてしまうので ライブラリが絡むとさらに辛くなります複雑なライブラリは複雑な型定義になってることが多いです
特に JavaScript を前提に作られてるものだと動的な考え方なので無理に静的に型付けしようとするとキレイには書けないことが多く なぜ動いてるのか理解がとても難しいものになってることが多いです
そういうのだと ドキュメントにあるような基本的な使い方のうちはいいものの 発展的なことをして使う側も複雑になってくると動きそうなものなのに型エラーが出て上手く動かないことが多いです
上でも書いたように わかりやすいエラーメッセージがあり修正できるならまだいいのですが かなり長く意味不明なエラーメッセージになることが多いです
ライブラリが動的にいろいろなパターンの入力で動くようになっているものでは いろいろな推論結果のエラーですし 長すぎるとエラーメッセージが変に省略されていますし エラーメッセージを見てどうにかするのは諦めてコードを見直すほうがいいケースも多いです
自分で書いた部分なら期待と違う方に推論された結果のエラーメッセージでもある程度は察せますが ライブラリだとそうはいかないです
JavaScript としての処理に加えて 型定義までライブラリの中身を読まないといけないのは面倒が多いです
これは使うライブラリの問題でもあるので 最初から静的型付けを想定した設計で作られていて 型定義もキレイなものだとそこまで困りません
ただ 私の場合は自分から進んで TypeScript を使わないので 基本的に自分で作ったものをいじるわけではないです
なので使われてるライブラリは自分で選んでないのでどうしようもないのですよね
自分で選ぶなら TypeScript を採用して作るなら静的な言語なんだからと割り切って 動的な考え前提で書かれているライブラリは使わないほうが苦労が減ると思います
動かなかったケース
あまりはっきりとした原因がわからず早々に諦めて別の方法にしたものですが React 関係で起きたものですシンプルなケースだとこういうのがあります
const fn1: (callback: (value: number) => void) => void =
callback => {
callback(1)
}
const fn2: (callback: (value: string) => void) => void =
callback => {
callback("a")
}
const callback: (value: number | string) => void =
value => console.log(value)
fn1(callback)
fn2(callback)
関数を受け取る関数 fn1, fn2 があって そこに渡す関数は型が違っても同じでいいから callback 関数を渡しています
callback 関数は両方のパターン number | string が引数でこれで問題なく動いています
問題のケースは React の onChange です
event や string や number などいろいろなケース を union 型でまとめた callback 関数相当のものを複数のイベントのリスナとして設定しようとしたらエラーでした
UI ライブラリとかも入ってきているので 変な方向に推論された結果だと思います
エラーメッセージは HTML の属性が足りないとか全く関係ない内容で HTML のタグ関係の型がどうこういうエラーでした
TypeScript ではなくライブラリの型定義の方に問題があるのかもしれないですが 現にそういうライブラリは有名どころでも普通にあります
TypeScript を使わず JavaScript ならそもそも発生しないのでこれも TypeScript の不満点です
とりあえず union 型にせず unknown や any として受け取ればエラーにはならないので 内側で型チェックすることで回避はできました
とりあえず any で対処できるかとおもいきや 逆で推論時に any だと他の型との判定ができずにエラーにされて明示的に型を書かざるを得なくなったときもありました
string 型とリテラル
「"foo" | "bar"」 みたいにリテラル型の union 型で書くと enum 代わりになって便利ですが リテラルでそのまま渡さないと string 型になってしまってエラーになることが多いです例えば
const fn: (values: { foo: number, bar: "A"|"B" }) => void =
values => console.log(values)
const values = { foo: 1, bar: "A" }
fn(values)
最後に fn に values を渡すところで string を "A" | "B" に代入できずエラーです
型を明示するか const 指定が必要になります
intersection 型で上書きできない
プロパティを追加したいときに使える intersection 型ですが 順番問わずマージされますtype A = { foo?: number, bar?: string }
type B = A & { foo: string }
と書いたときに A に { foo: string } を追加して foo は重複するので後ろの string で上書きされてほしいのですが number & string は存在しないので never 型になります
const value: B = { foo: "", bar: ""}
は foo が never 型への代入となってエラーです
JavaScript のオブジェクトのように ... が使えるといいのですが 構文エラーでした
type A = { foo?: number, bar?: string }
type B = { ...A, ...{ foo: string } }
実現には A から foo を除外した型を作って その型と intersection で結合します
type A = { foo?: number, bar?: string }
type B = Omit<A, "foo"> & { foo: string }
関数のように扱えるジェネリクスを作ることもできますが こういうことを始めるとどんどん複雑になっていき JavaScript では不要な苦労を味わうことになってきます
type Override<Base, Extend> = Omit<Base, keyof Extend> & Extend
type A = { foo?: number, bar?: string }
type B = Override<A, { foo: string }>
ちなみに関数だと引数の型が違っても通りました
オーバーロード用でしょうか
type A = { foo?: (a: number) => void, bar?: string }
type B = A & { foo: (a: string) => void }
const value: B = { foo: (x) => {}, bar: "" }
union 型とプロパティ
プロパティがあれば という分岐が面倒ですtype A = { a: string }
type B = { b: string }
const fn: (value: A|B) => void =
value => {
if (value.a) {
console.log(value.a)
}
}
こういう感じのコードを書きたいことは多いのですが B の場合は a プロパティがないからエラーです
B はオブジェクトなので null のプロパティ読み出しみたいにエラーにはならないのに許可されません
in を使って
if ("a" in value) {
console.log(value.a)
}
にしないといけないのですが JavaScript で in はほぼ使わないものなのであまり使いたくないです
それにこういうケースでは a プロパティがある A のパターンでも空文字など falsy なら if に入る必要がないというケースが多いです
in ではそのチェックはできないので a があるかチェックしてさらに falsy でないチェックをしないといけません
2 段階になって手間が増えすぎです
デフォルト値を使うと union 型の絞り込みが正しくない
こういうコードがありますtype Data =
| { type?: "foo", value: number }
| { type: "bar", value: string }
const fn: (data: Data) => void =
({ type, value }) => {
if (type === "bar") {
console.log(value.toUpperCase())
}
}
type の省略時は foo なので扱いやすいようにデフォルト引数で foo を入れます
type Data =
| { type?: "foo", value: number }
| { type: "bar", value: string }
const fn: (data: Data) => void =
({ type = "foo", value }) => {
if (type === "bar") {
console.log(value.toUpperCase())
}
}
こうすると type が bar のときに value が string と判断してくれません
number | string の union 型になって string のメソッドが使えません
型を再定義すればできなくもないですが すごく手間ですし TypeScript の都合で読みづらくなります
type Data =
| { type?: "foo", value: number }
| { type: "bar", value: string }
const fn = (data: Data): void => {
type DataFixed =
| { type: "foo", value: number }
| { type: "bar", value: string }
const { type, value } = {
type: data.type ?? "foo",
value: data.value
} as DataFixed
if (type === "bar") {
console.log(value.toUpperCase())
}
}
union 型と if
上のと似たようなもので こういうものでも型エラーになりますtype V =
| { type: "foo", list: string[] }
| { type: "bar", list: number[] }
const fn: (value: V) => void =
(value) => {
const result = [...value.list]
if (value.type === "foo") {
result.push("")
fn2(result)
} else {
result.push(0)
fn2(result)
}
}
const fn2: (x: string[] | number[]) => void =
x => {}
result は (string | number)[] という型になるので push はできますが string[] または number[] ではないので fn2 に渡せません
if ブロックの中で代入すればそれぞれの型になりますが冗長です
const fn: (value: V) => void =
(value) => {
if (value.type === "foo") {
const result = [...value.list]
result.push("")
fn2(result)
} else {
const result = [...value.list]
result.push(0)
fn2(result)
}
}
こういうケースもあります
type V =
| { type: "foo", list: string[], callback: (value: string[]) => void }
| { type: "bar", list: number[], callback: (value: number[]) => void }
const fn: (value: V) => void =
(value) => {
value.callback(value.list)
}
list と callback は同じ型ですがエラーになります
type V =
| { type: "foo", list: string[], callback: (value: string[]) => void }
| { type: "bar", list: number[], callback: (value: number[]) => void }
const fn: (value: V) => void =
(value) => {
if (value.type === "foo") {
value.callback(value.list)
} else {
value.callback(value.list)
}
}
にすればエラーは回避できますが さっき以上にムダな感じがします
このケースだと type は使わないですし
type V<T> = { list: T[], callback: (value: T[]) => void }
const fn: <T>(value: V<T>) => void =
(value) => {
value.callback(value.list)
}
と引数型を変更すれば 処理部分はシンプルにできますが 実際にはもう少し関数の本体部分が長くてこういう風にできないことも多いのですよね
union 型と Omit
union 型を含むものを Omit すると思い通りにならないですtype A = { foo: string, bar: string }
type B = { foo: number }
type C = { baz: number }
type AB = A | B
type ABC = AB & C
type Xfoo = Omit<ABC, "foo"> // { baz: number }
type Xbar = Omit<ABC, "bar"> // { foo: string|number, baz: number }
type Xbaz = Omit<ABC, "baz"> // { foo: string|number }
期待する結果は分解した状態でそれぞれから指定のプロパティを除いたものです
type Xfoo = ({ bar: string } | {}) & { baz: number }
type Xbar = ({ foo: string } | { foo: number }) & { baz: number }
type Xbaz = ({ foo: string, bar: string } | { foo: number }) & {}
union 型はすべてに含まれるプロパティだけ取り出した上で Omit 対象を除外したような動きになるようです
enum と union 型
TypeScript には enum 型があるみたいですが あまり使いたくないので union 型にします昔ながらの enum 型に近いのだとこういう使い方でしょうか
const answer = {
YES: 1,
NO: 0,
} as const
type Answer = typeof answer[keyof typeof answer]
const fn1: (a :Answer) => void =
a => {
console.log(a)
}
fn1(answer.YES)
fn1(answer.NO)
fn1(1)
Answer は answer の value の union 型なので 1 を直接渡せてしまいます
それだとミスで別の値を渡してもちょうど 1 などの数字なら通ってしまいます
そういう意味ではキーの文字列のほうがいいのかもしれません
const answer = {
YES: 1,
NO: 0,
} as const
type AnswerKey = keyof typeof answer
const fn2: (a :AnswerKey) => void =
a => {
console.log(answer[a])
}
fn2("YES")
fn2("NO")
推論が微妙
こういうことがやりたいですconst fn = (
{
value,
callback,
}:
{
value?: {},
callback?: (values: any) => void,
}
) => {
if (callback) {
callback({
x: 1,
...value,
})
}
}
fn({
value: { y: 10 },
callback: ({ x, y }) => console.log(x, y),
})
// 1 10
value や callback の型をちゃんと書いてないのでこれにちゃんとした型を指定します
const fn = <T>(
{
value,
callback,
}:
{
value?: Omit<T, "x">,
callback?: (values: T) => void,
}
) => {
if (callback) {
callback({
x: 1,
...value,
})
}
}
fn({
value: { y: 10 },
callback: ({ x, y }: { x: number, y: number }) => console.log(x, y),
})
これだと T が { x: number } と関係ないところでインスタンス化されるかもというエラーになりました
T に { x: number } が含まれる制限をつけるために T の条件として { x: number } を extend するように指定しましたがそれでも T のサブタイプかもみたいなエラーです
結局 引数として渡す値を as で T とみなすことで動かせました
const fn = <T extends { x: number }>(
{
value,
callback,
}:
{
value?: Omit<T, "x">,
callback?: (values: T) => void,
}
) => {
if (callback) {
const t = { x:1, ...value } as T
callback(t)
}
}
fn({
value: { y: 10 },
callback: ({ x, y }: { x: number, y: number }) => console.log(x, y),
})
ただの関数だと簡単な対策で済みましたが React の props だと面倒になりますし 視認性も落ちます
上のケースでさらに callback の引数が x のみで value が空になるなら省略可で そうじゃない場合は省略不可にしたいです
そうなってくると型の条件分岐が必要になって複雑です
const fn = <T extends { x: number }>(
{
value,
callback,
}:
keyof Omit<T, "x"> extends never
? {
value?: {},
callback?: (values: T) => void,
}
: {
value: Omit<T, "x">,
callback?: (values: T) => void,
}
) => {
if (callback) {
const t = { x: 1, ...value } as T
callback(t)
}
}
fn({
value: { y: 1 },
callback: ({ x, y }: { x: number, y: number }) => console.log(x, y),
})
fn({
callback: ({ x }: { x: number }) => console.log(x),
value: {},
})
型定義不要の JavaScript の fn 関数と比べるとすごく長く読みづらいです
const fn = ({ value, callback }) => {
if (callback) {
callback({ x: 1, ...value })
}
}
JavaScript の上の関数をみるだけで value と callback の関係は十分わかります
key value 形式の定数が扱いづらい
map のような使い方をするオブジェクトでキーの値が目的の型になってないことは多々ありますそのときに as でキャストが必要で面倒です
const langs = {
ja: "japanese",
en: "english",
} as const
const fn: (s: string) => void =
(s) => {
return langs[s as keyof typeof langs]
}
手抜きで any としても string や number はキーとして不正なのでエラーになります
const fn1: (s: string) => void =
(s) => {
return langs[s as any]
}
any なら何でも通してくれていいのですけど
langs の方を any にすれば通りますが () が入って見づらくなります
const fn2: (s: string) => void =
(s) => {
return (langs as any)[s]
}
ReactNode をそのまま返せない
React を使うとき children の型は React.ReactNode ですJSX でコンポーネントとして使う関数は返り値が Element | null である必要があるらしく undefined の可能性のある ReactNode だとエラーです
import React from "react"
const App = () => {
return (
<div>
<Component value={1}>
1
</Component>
</div>
)
}
const Component = (
{
children,
value
}: React.PropsWithChildren<{ value: number }>
) => {
if (value) {
return children
} else {
return <div>error</div>
}
}
この回避のために children を <></> でラップしないといけません
return <>{children}</>
JavaScript では不要な手間が必要になります
これは TypeScript 自身ではなく React ライブラリの型定義の問題ですね
関数を外側スコープに持っていくときの型定義が面倒
複雑な引数を受け取り その型に応じて返り値の型が決まる関数がありますconst fn = () => {
const value = complexFn({
a,
...b,
c,
...(condition ? then_value : else_value),
...something()
})
const value2 = convert(value, x, y, z)
}
このときの convert 関数の定義で value の型が必要になります
複雑な処理の結果生まれるものなので 静的に定義するのは難しく 変わりやすい部分なので引数に変更があるたびに型定義の変更は面倒です
fn の中なら typeof value で型を取得できるので容易ですが 型以外に fn の中の値を参照しないなら convert 関数は外側のスコープに持っていきたいです
complexFn 関数の呼び出し部分だけを取り出して
const _fn = () => {
return complexFn({ a, ...... })
}
type ComplexType = ReturnType<typeof _fn>
みたいに型情報取得のための関数を用意してそこから型情報を取得することはできます
ですが TypeScript の都合でムダな関数を作るわけですし スッキリとしない方法です
自分で引数の複雑な組み合わせを解決しなくても同じ式をコピペするだけで TypeScript がやってくれるのですが 二重に書かないといけないことに変わりはありません
また 上の例の a や b も fn が引数として受け取る値をもとに関数を通して決まる型ということも少なくないです
事前に型定義ありきで作られたものではなく 必要なものを全部詰め込んで動的に作っていくようなものと相性が悪いです
Object.entries で unknown になる
リテラルや Record 型を Object.entries に入れると期待通りの型になるのに string を継承した T を key に入れると value が unknown になりますconst value1 = Object.entries({a: 1, b: 2})
const fn1 = (arg: Record<string, number>) => {
const value2 = Object.entries(arg)
}
const fn2 = <T extends string>(arg: Record<T, number>) => {
const value3 = Object.entries(arg)
}
value1 と value2 は [string, number][] という型です
それに対して value3 は [string, unknown][] です
ジェネリクスの通り [T, number][] となって欲しいのですけど
Object.fromEntries が賢くない
オブジェクトに対して配列の map 関数みたいなことをしたいことはよくあります直接的な関数はないので Object.entries で配列化してから map したあとに Object.fromEntries でオブジェクトにします
しかしこのときに TypeScript だと型情報が思い通りになりません
const a = {
x: 1,
y: 2,
}
const a2: typeof a = Object.fromEntries(
Object.entries(a).map(([k, v]) => [k, v * 2])
)
これは a2 への代入でエラーになります
右辺の型は { [k: string]: number } となっているので x や y があるか判断できないためです
JavaScript 的にはよくある処理なのにこれをやるだけで型で上手く動かないのはかなり使いづらいです
as でキャストすれば通せますが 安全ではないです
こういうのでも通せてしまいます
const a = {
x: 1,
y: 2
}
const record: Record<string, number> = { z: 3 }
const a2: typeof a = record as typeof a
このときの a2 に x はないので a2.x.toFixed() みたいなことをすると型エラーは出ないのに実行時エラーが起きるコードになります
あとから処理を書き換えたときに as が問題ないかもチェックが必要になってきます
Object.entries の型
上の問題はそもそも Object.entries が正しく型情報を作れないからですconst obj = {
a: 1,
b: "",
c: true,
}
const entries = Object.entries(obj)
としたときの entries の型が
[string, string | number | boolean][]
となっています
期待する型はこういうのです
const obj = {
a: 1,
b: "",
c: true,
}
type ObjType = typeof obj
type Entries = {
[x in keyof ObjType]: [x, ObjType[x]]
}[keyof ObjType][]
// (["a", number] | ["b", string] | ["c", boolean])[]
そうなるような objectEntries を自作します
type Entries<T> = {
[x in keyof T]: [x, T[x]]
}[keyof T][]
const objectEntries = <T extends {}>(obj: T) => {
return Object.entries(obj) as Entries<T>
}
const obj = {
a: 1,
b: "foo",
c: new Date(),
d: /foo/g,
}
for (const [k, v] of objectEntries(obj)) {
if (k === "a") {
console.log(v.toFixed(2))
} else if (k === "b") {
console.log(v.repeat(2))
} else if (k === "c") {
console.log(v.getFullYear())
} else {
console.log(v.flags)
}
}
k を a や b で絞り込むと v の型が正しく判断されてメソッドが使えています
ただこれを map するともとのような形で残りませんし fromEntries でキレイにオブジェクト型に戻りません
分割代入時に一部の型だけをキャストできない
分割代入時にキャストができないので余計な変数を作らないといけませんtype Fn = () => ({ name: string, value: unknown })
という型の関数があります
const { name, value } = fn()
結果を分割代入すると value は unknown になります
value は number なので number にしたいのですが value に代入された時点で value の型が unknown に決まってしまうので扱いづらいです
const { name, value: _value } = fn()
const value = _value as number
のように仮の変数を作ってからキャストになります
TypeScript の都合で JavaScript では不要な処理です
変数を作らない場合は代入前のキャストですが 一部だけをキャストできません
const { name, value } = fn() as { name: string, value: number }
のように name も必要です
1 つくらいならまあいいのかもですけどプロパティが多い場合にはやってられません
本来の fn の返り値の型から value を除外した型と { value: number } の型を & でつなげばマシですが 面倒なことに変わりありません
1 つめの引数の型をベースに 2 つめの型で上書きした型を返す Override ジェネリクスを用意して書いても↓です
const { name, value } = fn() as Override<ReturnType<typeof fn>, { value: number }>
一旦 unknown で受け取ってから別変数にキャストして入れたほうが見やすいと思います
プロパティの型を受け取れないことがある
ライブラリ側の型定義方法のせいなのでしょうが プロパティの型情報がうまく扱えないケースがありますconst foo: Foo = {
prop: "foo",
}
形式でプロパティを書こうとすると prop が候補に出てきて 型情報も取得できます
しかし その型情報を取得して別名にしようとするとエラーになります
type P = Foo["prop"]
で取得できません
実際に確認可能なものは MUI の SxProps で確認できます
import { SxProps } from "@mui/material"
const sx: SxProps = {
display:"flex"
}
type Display = SxProps["display"]
sx のオブジェクトの中身を書くときに display を補完で出せますし flex や block などは選択肢から選べます
なので SxProps に display プロパティがあると認識されてそうなのですが 下側のように Display に代入しようとすると display プロパティはないというエラーになります
npm ライブラリの import があっても TS Playground で確認できました
TS Playgound
ジェネリクス関係な気もしてますが どちらも推論できるだけの情報がないはずなんですよね
デフォルト値ならどっちも同じになりそうですけど
バージョン管理との相性
Git を使ってる場合 ブランチを切り替えたときに問題が出ることが多いです1 つはソースコードが切り替わったのに型チェックではうまく新しい情報に切り替わっていないようで 正しいはずのところでエラーになります
Git 以外でもツールが自動でコードを生成したときなどにも起きます
CRA とか Vite などの監視して更新するツール側でもそこそこありますが VSCode ではより頻繁に起きます
待ってても改善しないことがほとんどなので TS Server の再起動で対処してます
もう一つは必要ないところでも型がチェックされることです
開発中にあれこれ試すためのコードをバージョン管理外のファイルで用意してることが多いです
新機能を作ってるブランチで バージョン管理外のコードから新機能のモジュールや関数を参照します
その状態で今の main ブランチでの動作を確認したいなと思って切り替えることが結構あります
バージョン管理外のコードはブランチ切り替えに影響はしないですが 切り替え後には新機能のモジュールや関数がないので静的解析でエラーになってしまいます
バージョン管理外のコードの中身を一旦どこかに退避してから 全部消してやっと動くようになります
確認後にはブランチを戻して バージョン管理外のコードをもとに戻してとすごく手間になります
ジェネリクスの配列
2 つのプロパティが同じ型になるオブジェクトがありますtype Foo<T> = {
value1: T,
value2: T,
}
value1 と value2 は型が同じである必要があります
同じであれば何でもいいです
これを配列化するとき T は配列の要素ごとに異なる可能性があります
1 つめは number のペアで 2 つめは string のペアなどです
しかし T が 「number | string」 となると 1 つの Foo の中で value1 が string で value2 が number というのも許可されてしまいます
実現不可能ではないらしいですが 単純な方法ではできず チェック用の関数を通す必要があったり 手間が多くやりたい方法ではないです
TypeScript の都合で Foo のオブジェクトを作るたびに余計な関数を通すことになりますし
実際に これそのままのケースだとあまりないですが プロパティの 1 つが関数で その引数の型がもう一方のプロパティというケースはときどきあります
React だと Component と props のペアをまとめて保持するときにこうなります
なのにそれを簡単に解決できないのは言語機能が十分でないと思うんです
in を使いたくない
TypeScript では存在しないプロパティへのアクセスが許可されないので in を使ってチェックしないといけないことが多くなりますif ("key" in obj) {
console.log(obj.key)
}
in は undefined でもキーがあれば true になるので undefined の扱いがさらに面倒になります
ほとんどの場合 キーがあって undefined か キーがないかは気にしなくていいいです
プロパティにアクセスして undefined かどうかをみればいいです
if (obj.key) {
console.log(obj.key)
}
キーがあってもなくても undefined が得られます
なのでプロパティを消したいときに delete 演算子は使わず undefined を代入します
V8 だとキーの追加や削除より上書きするほうがパフォーマンスに優れるとも聞きますし
しかし TypeScript はこれができないので in にする必要があり in だと undefined を入れても true になるので delete を使わないといけなくなります
delete なんて with と同じくらい使わないものなので それを使わざるを得なくなるのが嫌なところです
実際に困る例はこういうのです
type A = { foo: string }
type B = { bar: string }
const fn: (obj: A|B) => void =
(obj) => {
if ("foo" in obj) {
console.log("A", obj)
} else {
console.log("B", obj)
}
}
const t = { foo: undefined, bar: "" }
fn(t)
// A { foo: undefined, bar: "" }
t では B の方の型として扱われることを期待するのに A として扱われてしまっています
if (obj.foo !== undefined) {
// ...
}
と書ければ問題ないのですけどね
union 型の判断を言語がやってくれない
上で書いたような in を使う if や type プロパティを見るなどで型を判断するしかありませんtype A = { p1: number }
type B = { p2: number }
type AB = A | B
const fn: (v: AB) => void =
(v) => {
if ("p1" in obj) {
// A
} else {
// B
}
}
type A = { type: "a", value: number | string }
type B = { type: "b", value: number | null }
type AB = A | B
const fn: (v: AB) => void =
(v) => {
if (v.type === "a") {
// A
} else {
// B
}
}
やりたいことは直接的に
const fn: (v: AB) => void =
(v) => {
if (v is A) {
// A
} else {
// B
}
}
です
複雑な型になってくると自分で判定する関数を用意したくないので 内部的に判断できてるなら TypeScript に任せたいものです
ですが 実行時に型情報がないのでそういうことができません
せめて判定する関数を自動で生成して is キーワードを使ったらその関数を呼び出して判定するような変換くらいはしてほしいものです
型定義に関数の引数名はいらない
関数の型を書くときはこういう形ですtype A = (arg1: number, arg2: string) => void
引数は型情報だけでよく
type B = (number, string) => void
だけで良いはずです
それにここでいう B の型定義は使い回すことがあり number 型が長さを指すこともあれば回数を指すこともありえます
型定義の時点で名前は決まらず とりあえず arg1 とか意味のない名前になることも少なくないです
書くことができるのはいいですが 必須にされても面倒なだけです
ただ面倒なだけでなくネストするととても見づらいです
type C = (fn: (arg: number) => number) => void
名前が不要ならもっとスッキリします
type D = (number => number) => void
引数の分割代入と型定義
関数の引数で分割代入機能を使うと見にくいです型定義を別に書くならあまり気になりません
const fn: (arg: { x: number, y: number }) => number =
({ x, y }) => x + y
しかし 直接引数のところに書くと
const fn = ({ x, y }: { x: number, y: number }) =>
x + y
オブジェクト全体の型として分けて書かないといけないです
通常の引数で
const fn = (x: number, y: number) => x + y
と書けるように各プロパティの横に書きたいです
const fn = ({ x: number, y: number }) => x + y
しかし この構文の : は JavaScript として意味があるものなので別の意味として解釈されます
JavaScript に継ぎ足ししたような言語にせず 型付きの言語として 1 から構文を考えて作ってほしかったです
現状の書き方ではオブジェクトのプロパティが多いと変数と型情報が離れすぎていて分かりづらくなります
型のデフォルト値を使えない
こういうコードを書きたいことがありますconst required = <T,>(value: T|undefined) =>
value === undefined ? default(T) : value
しかし JavaScript で使われる実体と統合されないので 型 T のデフォルト値を取得ということができません
存在しないプロパティへのアクセスができない
union 型なら if 文で in を使ってアクセスできましたが プロパティ定義に完全にない場合は in を使ってもアクセスできませんtype A = { a: number }
const fn: (param: A) => void =
(param) => {
console.log(param.a)
if ("foo" in param) {
console.log(param.foo)
}
}
これは param.foo のアクセスがエラーになります
プロパティが存在するチェックをしてるのだから any 型ででも受け取れればいいのにできません
foo を持つ型を用意してそれへのキャストが必要になります
type A = { a: number }
const fn: (param: A) => void =
(param) => {
console.log(
(param as A & { foo?: number }).foo
)
}
見づらく面倒です
またこの場合は A という一文字になっているのでまだマシで param: のあとに直接オブジェクト定義を書いているともっと面倒になります
~~obj.count
みたいにして 値が無いならまとめて 0 として扱いたいケースは多々あるのですけどね
ライブラリなどで不親切な型定義だと X があるなら Y もあるはずみたいなのを適切に表現してないので個別に対処しないといけないことあってとても面倒です
動的なキーの除外
オブジェクトから特定のキーを除外したいことはよくあります型的にそれをやろうとするととても面倒です
静的にわかるものなら Omit が使えますが 引数で渡されるキーを除外したい場合に困ります
引数がリテラルなら T として受け取れるのですが 配列に入っていて string という情報だけだとたぶんどうしようもないです
const obj = { a: 1, b: "" }
const exclude_keys = ["a"]
const obj2 = exclude(obj, exclude_keys)
// { b: "" }
このときの exclude 関数をうまく定義できないです
インターフェースを変えてキーの配列ではなく 除外したいキーをキーとして持つオブジェクトならマシかもしれません
const obj = { a: 1, b: "" }
const exclude_obj = { a: null }
const obj2 = exclude(obj, exclude_obj)
// { b: "" }
使いやすさはイマイチで無意味な value も書く必要があるのですが オブジェクトなら配列と違ってキーは保持されるので 除外すべきキー情報を引数の型から特定できます
type ConvertFn<A1, A2> = (a1: A1, a2: A2) => Omit<A1, keyof A2>
type X = ReturnType<ConvertFn<{ a: number, b: number }, { a: null }>>
// { b: number }
ただ完全に TypeScript の都合なので それのために JavaScript としての処理を変えるのはあまりやりたいとは思えません
関数を通すと型の絞り込み情報が消える
こういう関数がありますconst fn = (obj: { x: "foo" | "bar" }) => {
if (obj.x === "foo") return
return obj.x
}
// return "bar" | undefined
foo の場合は即 return してるので返り値は bar または undefined です
返り値の型でこれが正しく推論できています
最後の処理で関数を使って処理します
const fn2 = (obj: { x: "foo" | "bar" }) => {
if (obj.x === "foo") return
return ((arg) => {
return arg.x
})(obj)
}
// return "foo" | "bar" | undefined
消えたはずの foo が復活しています
関数内の arg の時点では x が "foo" | "bar" に戻っています
その場で作って実行する即時関数なので直前の状態を反映して欲しいですがそういうことはできないようです
obj ごと渡さず obj.x だけなら絞り込み状態の型になるみたいです
絞り込まれてるのは obj.x の型であって obj 型自体は変わらずということみたいです
オブジェクトのプロパティなら変数と違って別のどこかで書き換えられて絞り込んだのが意味なくなることがあるから?とか考えても見ましたがやろうとすると現状の絞り込みでも実行時エラーを起こせます
const obj = {
_foo: true,
get x() {
const foo = this._foo
this._foo = !this._foo
return foo ? "foo" : "bar"
},
}
このオブジェクトの x プロパティは読み取るたびに foo と bar が切り替わります
console.log(obj.x) // foo
console.log(obj.x) // bar
console.log(obj.x) // foo
console.log(obj.x) // bar
これをさっきの関数に入れると
const fn = (obj: { x: "foo" | "bar" }) => {
if (obj.x === "foo") return
return obj.x
}
console.log(fn(obj)) // undefined
console.log(fn(obj)) // foo
のようになって推論された "bar" | undefined 以外の値が返ってきてます
なので特別な意味があるというよりはそこまでサポートしてなくて ただ不便なだけだと思います
ありえない型を除外できない
仕組み上 仕方ない気もしますが union 型で絶対に来ないものも手動で除外しないといけない場合がありますconst f1 = (a: string | null) => {
a = ""
a.slice()
}
const f2 = (a: string | null) => {
if (true) {
a = ""
}
a.slice()
}
const f3 = (a: string | null) => {
const x = true
if (x) {
a = ""
}
a.slice()
}
f1 と f2 は a が string と確定するので slice メソッドの呼び出しが可能です
f3 も f2 と同じかと思いきや if 文が実行されないケースがあると判定されて a.slice() でエラーです
x は boolean 型となって true として判断してくれません
絶対来ないのに TypeScript のために if 文が必要です
const f4 = (a: string | null) => {
const x = true
if (x) {
a = ""
}
if (a === null) return
a.slice()
}
もっとよくあるのは外側スコープの変数を参照するときに 関数が呼び出されるなら nullable ではないというパターンです
const outer = () => {
const value = fn() as string | null
const inner = () => {
console.log(value.slice(1))
}
// ...
}
inner 関数を呼び出す側で value が null にはならないときのみ呼び出すのですが それを TypeScript は理解できないので inner の中で value のチェックをしないといけません
UI を作るときに disabled を使って制限してることはけっこうあります
<button disabled={!value} onClick={inner}>OK</button>
nullable なら value!.slice() のように ! をつけることができて少し楽です
しかし他の型だと自分で判定して if 文を作るか as でキャストする必要があります
トップダウンで書くのに向いてない
コードを書くときに ライブラリ的な小さい機能を先に用意してから それを使って全体の処理を書くボトムアップ的な書き方と 先に全体を書いて後から内部の処理を書くトップダウン的な書き方がありますウェブサーバーで言えば 先にリクエストのデータのバリデーション処理やデータベースから取得や保存の処理を書いて 最後にそれらを使ったリクエストハンドラの処理を書くのが前者です
先にリクエストハンドラでやること全体を書いて その中のデータベースに送るクエリなどを後で書くのが後者です
TypeScript など静的型付け言語だとボトムアップの方法が向いています
すでに用意してるパーツなら補完と相性がよく インポート文や関数名などで補完が使えます
それに対してトップダウンだと存在しない関数やモジュールが存在する前提で書いて 後からそれを書くので補完はできません
それどころか存在する中から似ている名前のもので無理やり補完されてしまうなどもあって迷惑です
知らない間によくわからないモジュールのインポートがファイルの最初に追加されていたなんてこともあります
また 存在しない関数や型を書くとエラーを表す赤色の線が画面に出ます
最初だとほとんどの関数は存在しない状態なので 赤線だらけで気持ち悪いです
まだ作っていないものがわかるので便利といえば便利ですが わかりやすくするために変に目立つのでずっと出てると不快です
あのエラー通知はチェックするボタンを押したときだけ出るようにして欲しいくらいです
自分の場合は先に全体の処理をなんとなく書いてから細かいところを実装していくことが多いので 上記の通り相性が悪いです
typeof を使う場所
妥当な動きではあるもののハマったところで typeof の場所があります変数の型を取得できるのですが その時点でありえる型に制限されています
foo が 「Foo | null」 という型で foo が null のときのデフォルト値を as で Foo 型にしようとしたらうまく動きませんでした
const value = foo ?? ("X" as typeof foo)
?? の右側なので null か undefined です
この場合の型だと null になります
「as null」 となるのでだいたいの場合はここでキャストエラーです
期待してるのは Foo 型になることですがそうなってくれません
type TypeofFoo = typeof foo
const value = foo ?? ("X" as TypeofFoo)
のようにしないといけないです
Foo が型として定義されているならそれを使えばいいのですが 簡単に書ける形になっていなくて typeof のほうが早いケースはけっこうあります
分割代入すると推論される型が変わる
バグじゃないのと思うような挙動がありますtype XY = "x" | "y"
const s = ["x"] as XY[] | undefined
const b = true as boolean
const u = b
? { p: s }
: { p: s ?? ["x"] }
const up = u.p
// XY[] | undefined
const { p } = b
? { p: s }
: { p: s ?? ["x"] }
// XY[] | string[] | undefined
条件演算子で分岐して プロパティ p を持つオブジェクトを作っています
この結果を変数 u に代入して p のプロパティを取得するのと 分割代入で直接 p を取得するので型が変わります
一旦 u に入れると意図する 「XY[] | undefined」 という型で取れますが 分割代入だと string[] という型が入ってきます
s の型を XY[] から XY にして配列じゃなくしてみます
すると
type XY = "x" | "y"
const s = "x" as XY | undefined
const b = true as boolean
const u = b
? { p: s }
: { p: s ?? "x" }
const up = u.p
// XY | undefined
const { p } = b
? { p: s }
: { p: s ?? "x" }
// XY | undefined
どちらも同じ結果で string 型は入ってきません
単体だと
type XY = "x" | "y"
const s = ["x"] as XY[] | undefined
const b = true as boolean
const u = { p: s ?? ["x"] }
const up = u.p
// XY[]
const { p } = { p: s ?? ["x"] }
// string[]
分割代入は string[] になってしまってます
オブジェクト化を通さなければ
const t = s ?? ["x"]
// XY[]
XY[] になります
わけがわかりません
ここでまた配列ではなくしてみます
type XY = "x" | "y"
const s = "x" as XY | undefined
const b = true as boolean
const u = { p: s ?? "x" }
const up = u.p
// XY
const { p } = { p: s ?? "x" }
// XY
string にはなりません
どういう処理をしてるのかわかりませんが 分割代入は一旦オブジェクトに入れてそれを取り出す挙動のはずなので 型が変わるのはおかしいと思うんです
ありえない default は許容される
こういう関数を用意しますconst fn = (x: any) => {
if (x === 1) return "foo"
if (false) return "bar"
switch (1) {
case 1:
return "x"
case 2:
return "y"
default:
return "z"
}
return "baz"
}
return "bar" は条件が false なので絶対実行されません
また switch は 1 で値が固定なので 2 や default のケースに来ることもありません
switch で全パターンが return してるので最後の return "baz" も実行されません
到達しないコードは静的解析で Unreachable code としてエラーが表示されます
ただ default ケースではエラーにならないようです
実際はもっと複雑なコードで default があってもエラーにならないので 来る可能性があるのだろうなと思っていたものの どういうパターンがあるかを考えても思いつかず悩んでいたのですが TypeScript がエラーとして扱わないみたいでした
default ケースに来ることがないなら switch に渡した変数の型を default ケースの中で見ると never になっています
const b = true
switch (b) {
case true:
break
case false:
break
default:
b // never
}
確認のために一旦変数に入れないといけないのは少し面倒でもあります
ちなみに 絶対入らない default ケースの中でも return するとその型が関数の返り値の型として union 型に含まれます
オーバーロード関数が書きづらい
union 型で十分なことが多いですが A 型を渡すと B 型を返して C 型を渡すと D 型を返すみたいなルールを正しく表現できませんtype Fn = (arg: A | C) => B | D
だと A 型を渡して D 型が返ってくることもある型となり ありえない型が入ることによって使う側が面倒になります
別々の関数定義に分けて & すれば表現できるのですが 作るときに as が必要になってイマイチです
type Fn =
& ((p: "A") => number)
& ((p: "B") => string)
const fn1 = ((p) => {
if (p === "A") {
return 1
} else {
return ""
}
}) as Fn
const a1 = fn1("A")
const b1 = fn1("B")
関数の as は関数部分をカッコで囲まないといけないですし 書きやすさ見やすさ的に他関数より劣ってると思います
代入先に型情報を持たせる事も考えましたが エラーでした
const fn2: Fn =
(p) => {
if (p === "A") {
return 1
} else {
return ""
}
}
const a2 = fn2("A")
const b2 = fn2("B")
アップデートで苦労する
開発が活発で 色々と修正改善されているので困っていた部分がバージョンを上げると解決できたとかはありますただ 逆もあって TypeScript バージョンを上げるとこれまで問題なかったところで型エラーが起きることがあります
緩かった部分がより厳密になったとか デフォルトの設定値が変わったとかなんでしょう
最近だと 4.8 系に上げると多数の型エラーが起きたものがありました
特に外部ライブラリが関係するようなところでムリヤリ型を合わせてるようなことをしてたところだとかなりつらいです
TypeScript 的にエラーが出てないしこれでいいやと結構適当にやっていたのでしょう
自分でも JavaScript で書いてエラーが出なくする程度に型の補足を書く程度に使っているのでこういう問題が出ることはありそうです
ただ JavaScript 的には問題なく動くコードなのに TypeScript の都合でバージョンを上げて色々修正したりしないといけなくて時間をかけるのってすごくムダなことをしてる気持ちになります
条件付きで存在するプロパティを書きにくい
オブジェクトのプロパティが条件によって変わるときはこんな感じで書きますconst obj = {
value: 1,
...foo
? { bar: 1 }
: { baz: 2 }
}
else の場合はないことも多く JSX で
<div>
{foo && (
<Foo foo={foo} />
)}
</div>
と書くような感じで
const obj = {
value: 1,
...foo && (
{ foo }
)
}
と書くことも多いです
undefined や null や false などでは ... では何も展開されないので falsy な値で問題は起きません
スッキリかけて気に入ってる書き方なのですが TypeScript では型が違うとエラーにされます
配列だと null を ... で展開しようとすると JavaScript の実行時エラーなので わかりますが null を許容しているオブジェクトでまでエラーにするのは TypeScript の型定義がおかしいとしか思えません
そのせいで冗長なこんな書き方になります
const obj = {
value: 1,
...foo
? { foo }
: {}
}
find で取得後に再度 ?. が必要
こういう型と値がありますtype A = {
obj_or_undef?: {
str: string
}
}
const items: A[] = [
{ obj_or_undef: { str: "a" }}
]
items にはもっと多くの要素があるとして find で str がある最初の A を取得します
その A の中にある str を取得したいです
find で見つかった時点で obj_or_undef が undefined でないことは確実です
なのでこう書きたいです
const str = items.find(item => item.obj_or_undef?.str)?.obj_or_undef.str
しかし TypeScript のエラーになるので ?. にしないといけません
const str = items.find(item => item.obj_or_undef?.str)?.obj_or_undef?.str
?. があると null や undefined が来ることがある場所だと思ってしまうので null や undefined が確実に来ない場所で使うのは避けたいです
!. にもできますが余分な一文字があるのは同じですし チェックせず強制するものなので find の部分が変わったら問題が出る可能性があり積極的に使いたくないです
ちゃんと対処するなら find 関数が型をチェックするようにしてこうする必要があります
const str = items.find(
(item): item is { obj_or_undef: { str: string } } =>
!!item.obj_or_undef?.str
)?.obj_or_undef.str
流石に面倒で find の度にこんなのを書きたくはないです
別配列と find
次の処理で value1 の方は問題ないのですが value2 の方でエラーになりますtype A = { foo: string }
type B = { foo: number }
const items = [] as A[] | B[]
const value1 = items.map(x => x)
const value2 = items.find(x => x)
エラーの内容はこういうものでした
Each member of the union type '...' has signatures, but none of those signatures are compatible with each other.
find の方は異なる型の配列の union だとだめらしいです
map で動くなら find で動いてもいいと思うのですがエラーになります
items の型を配列同士の union 型から union 型の配列に変えると動くようです
const items = [] as (A | B)[]
const value3 = items.map(x => x)
const value4 = items.find(x => x)
配列同士の union の map で 値をそのまま返す value1 の結果が union 型の配列になるので
const items = [] as A[] | B[]
const value5 = items.map(x => x).find(x => x)
のようにすればエラーなく動かせますが ムダな処理が増えてます
そのまま find したときに 引数が A | B になって 返り値が A | B | undefined でいいと思うのですけどね
ジェネリクスの推論は intersection 型を考慮してくれない
こういうときtype A = {
foo: string,
bar: string,
}
const fn = <T,>(a: A & T) => {}
fn({
foo: "",
bar: "",
baz: "",
})
T は baz だけのオブジェクトになってほしいけど foo, bar, baz 全部を含んだオブジェクトになる
全部含む型で受け取って Omit するしかなさそう
型の一時変数が使えない
variable とか let とかでググっても通常の変数の話ばかり出てきてググりづらいのでもしかするとあるのかもしれませんが自分が知る限りない機能です型定義時に 色々なジェネリクスが入って同じものを複数箇所で使う場合は一時変数に入れたくなります
type X<A, B> = { x: A | B }
type Y<A> = { y: A }
type Z<A, B> = { z: A | B }
type Foo<A, B> = Z<X<A, B>, Y<X<A, B>>>
だと
type Foo<A, B> =
type XAB = X<A, B>
Z<XAB, Y<XAB>>
みたいなことがしたいですが これができません
同じ型でも毎回書く必要があって長い型になるととてもつらいです
Excel の数式を書いてる気分になります
Excel でも最近は LET 関数があるのでもっとマシです
一応できなくもない方法はあるのですが 良い方法とは言えないです
追加の型パラメーターのデフォルト値を使います
type Foo<A, B, XAB = X<A, B>> = Z<XAB, Y<XAB>>
type Foo1 = Foo<string, number>
// { z: X<string, number> | Y<X<string, number>> }
動きはしますが 3 つ目を渡すことができて 渡すとこうなります
type Foo2 = Foo<string, number, boolean>
// { z: boolean | Y<boolean> }
1, 2 番目の型パラメーターが完全に無視されます
関数で言えば
const fn = (a, b) => {
const c = a + b
return c * c
}
を
const fn = (a, b, c = a + b) => {
return c * c
}
と書くようなものです
できると言っても積極的にやりたいものじゃないです
やるにしても 書き方を工夫して 後半が一時変数の特殊な型パラメータであるとわかりやすくしておきたいものです
type Foo<
A, B,
Tmp1 = X<A, B>,
Return = Z<Tmp1, Y<Tmp1>>
> = Return
おわり
とりあえずこんな感じで 不満点がとても多いです追加があればしばらくの間はこの記事を追記するかもしれません