なぜ型定義で実際に返される型のUnion Typeになっていないのか
前提
Discord.jsの client.channels.get(id)
は本来、 TextChannel
や DMChannel
などの実際に存在する対象を返す
ただ型定義上は、チャンネルという共通概念である Channel
を返すようになっている
ドキュメント上でも同じようになっているけど、これは普通に書き換えたほうがわかりやすい気がする
実際には TextChannel
であっても、型定義で Channel
になっていると、型エラーが出てしまって面倒くさい
TypeScriptを使っていると通常ではなるべく避ける as
を使うはめになって、型安全か冗長化を選ばざるを得なくなる
なぜそうなっているのか
調査はしていないけど、ドキュメント上も Channel
を返すようになっているのを踏まえると昔からの成り行きっぽい
問題が浮き彫りにならなかったとか、ドキュメント上の型もなんとなく理解できてたとかで
一見問題なさそうに見えたけど、Union Typeのままでメソッド呼び出しができないという問題が発覚して
revertされたTypeScriptの型推論上の問題として、Union Typeのままだとメソッド呼び出しができなくなるという問題があった
この型推論の挙動はTypeScript 4.0で改善されそうな気配があって、そのうちまたUnion Typeになるかもしれない
なった

検証
新たなバージョンがリリースされる前にrevertされて、その変更を含まれたバージョンが存在しない
代わりにGitHubから該当コミットをインストールすることで体験できる: discordjs/discord.js#69d69f2
条件式の中で型が絞り込めるのがわかる(上が通常のバージョンで、下がUnion Typeあり)
挙動の深堀り
ただ実際に検証してみるとsetNameではなくthenで型エラーが出てることから単にメソッド呼び出しが問題ではなくてもっと複雑な因子が絡んでいそうな感じがした
channel変数は TextChannel | VoiceChannel
のような、GuildChannelを継承したクラスのUnion Typeになっていて、それらのクラスにはsetNameが定義されていなく、GuildChannel.setNameという扱いになってる可能性はありそう
つまりUnion Typeでも、それらの共通の親クラスのメソッドとして解決できると対象外となる...?
そしてsetNameを抜けてPromise付きで Promise<TextChannel> | Promise<VoiceChannel>
のような各クラスのUnion Typeになって、それぞれ独立した型に型に対するメソッド呼び出しで型エラーが出るといった感じっぽい
回避策
今の段階だと型を補完するために、自分で明示する必要があって、上にも書いたように as
を使う方法がある
ただ闇雲に as
を使うと間違った型に変えかねないから、多少冗長にはなってしまうがType Predicateのほうが安全
channel.type
でチャンネルの種類が取得できるのでこのように書ける(もちろんinstanceofを使ってもよい)
NewsChannelなどはTextChannelを継承しているけどtypeは"news"なのでinstanceofではなくてtypeを使ったほうがよい
tsconst isTextChannel = (channel: Discord.Channel): channel is Discord.TextChannel => channel.type === 'text'
関連