◆ 色々

ブログには何度か書いてますが 個人的には静的型付け言語が好きではないので TypeScript は極力使いたくないです
しかし 最近では 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

おわり

とりあえずこんな感じで 不満点がとても多いです
追加があればしばらくの間はこの記事を追記するかもしれません