Web開発ログ | エンジニアに役立つ情報 | 開発の中での気づきを発信

高卒Devlog

TSLab【第2回 TypeScript勉強会】今回のテーマは型定義の基礎!

thumbnail

こんにちは! RYOTAです!

当記事をご覧くださりありがとうございます!

当記事は定例になりましたTSLab勉強会のレポート記事になります!

はじめに

私が運営しているTSLabの第二回定例会を実施いたしました。
前回参加者に限定して招待したのですが今回も30人以上の方にご参加いただいたのでとても驚いております!
ご参加いただいた皆さま本当にありがとうございました!!!
前回同様にさまざまな議論が飛び交って私自身も新しい気づきがあったので簡単に説明していこうと思います!
下記に第1回の記事になりますので気になる方は合わせてご覧ください。
https://kosotudev.com/tslab-regular-1/

実施内容

参加メンバー

第1回目の勉強会に参加いただけた方、既にご連絡頂いていて前回ご参加できなかった方限定で招待をさせて頂きました。
TypeScriptを実務で使っている人は全体でも数名(私含めて)だけでその他はTypeScript未経験という方がほとんどでした。
金曜日の夜ということもあって参加者皆さん意欲的!いい人ばかりでやりやすい!マジで!

実施内容

前回同様TypeScriptの公式ドキュメントを読み合わせしてグループごとに議論しました。
前回は「TypeScriptってそもそも何なのか」「JavaScriptとは何が違うのか」「使うとどんなメリットがあるのか」といった概念であったり特徴の理解を進めていきました。
今回はTypeScriptの基本的な型や型定義の方法、挙動や特性について議論していったので具体例もありとても有意義な時間でした!

ディスカッション内容

各グループさまざまな議論が飛び交っていましたが、下記の議題が特にアツかったです!

  • Interface vs Type
  • 型推論と型注釈
  • 無名関数内の型推論

Interface vs Type
一番盛り上がったのがこちら。
型定義をする時にinterfaceで定義するかtypeで定義するか
TypeScriptの公式見解ではinterfaceを推奨していますが、それは昔のtypeでは出来ないことが多くてinterfaceの方がオブジェクトっぽく扱えるからなんですよね。
ただし今のTypeScriptでは出来ることに差がないので正直どちらも変わりません。(と言うと色々語弊がありますが。(笑)
interfaceを使う際は宣言のマージがあるので型の命名に注意をしないといけません。
例えばこんな感じでinterfaceで型定義をしてみます。

interface Test {
  hoge: string
  fuga: number
}

const test: Test = {
  hoge: 'ほげほげ',
  fuga: 1234,
}

これだけだと問題ないのですが途中で意図せずに同名で再定義してしまうと宣言のマージが起きてしまいます。
意図しているマージであれば良いですが、いつの間にか起きてしまうとエラーが出てしまうので注意が必要です。

interface Test {
  hoge: string
  fuga: number
}

// 意図せず再定義してしまった場合
interface Test {
  addHodeFuga: boolean
}

// エラー: プロパティ 'addHodeFuga' は型 '{ hoge: string; fuga: number; }' にありませんが、型 'Test' では必須です。ts(2741)
const test: Test = {
  hoge: 'ほげほげ',
  fuga: 1234,
}

これは一例ですがinterfaceとtypeどちらが良いと言う訳ではなく状況によって使い分けるのが良いのではないかというのが最終的に出た結論でした。
現にプロジェクトでinterfaceしか使っていないという方や、typeを主に使っている方に分かれたので実際にはプロジェクトのルールによって違うのではないかというのが見解です。
おまけ
今回の勉強会で初めて知ったのですが、typeエイリアスを使って型をextendした際に謎の挙動を示すことがわかりました。
下記のようにBefore.fugaをnumberで定義し、その後AfterにBeforeをextendする時にAfter.fugaをnumberからstringに変更します。(プロパティ名は変えずに型を変更する)
そうするとAfter.fugaがnever型として判定され、stringのvalueを割り当ててもエラーを吐いてしまうっぽいです。(マジで謎。)

type Before = {
  hoge: string
  fuga: number
}

type After = Before & {
  hoge: string
  // numberからstringに変えてみる
  fuga: string
}

const test: After = {
  hoge: 'hoge',
  // stringにしたはずが何故かnever型になる
  fuga: 'fuga'
}

何故こうなるのかは分かりませんね。
typeエイリアスの方が意図しないマージを防げると思っていたのですが、実際にはそんなこともないのかもしれないですね。(この辺りはもっと調べてみる必要がありそう。)

型推論と型注釈

複数のグループで盛り上がっていたのが「型推論に任せるべきか?」or「型注釈をなるべく付けるべきか?」
私自身は基本は推論に任せ、関数の引数と返り値は注釈をマストという認識でしたが、議論を進める中で注釈した方が読みやすいからやった方が良い派も一定数いらっしゃいました。
未経験の方は注釈と推論の使い分けポイントが不明瞭だから全部注釈でよくない?って感じでしたが経験者は基本的に推論で良いでしょという意見が多かったですね。

無名関数内の型推論

次に盛り上がっていたのが無名関数内の型推論。
これはJS/TSを使っていると直感的にわかりますが、今回は実務未経験の方もいらっしゃったので何ぞこれ?状態になってました。(笑)

const testArray = ['テスト', 123, true]

// 推論結果 -> (parameter) v: string | number | boolean
testArray.forEach((v) => {
  // 推論して実行前にエラーを出してくれる
  // エラー内容 -> プロパティ 'toUpperCase' は型 'string | number | boolean' に存在しません。
  console.log(v.toUpperCase())
})

無名変数の引数を良しなに解釈して勝手に推論をしてくれるTSちゃん、便利ってことですね!(雑)

最後に

第1回目ではTypeScriptの概要、第2回目(今回)では型の種類や基礎を題材にディスカッションをしましたが改めて言語化しようとすると難しいことや、分かっている風で理解しきれていない概念もあったのでとても学びが多かったです。
特にtype extendsの謎挙動。これは本当に知りませんでした。
TSに2年近く触れてるのに知らないことばかりですね。笑
議論を進めていく中で答えが出ないことも多かったのでその辺りは今後深掘っていこうと思います!
以上です!最後までご覧くださりありがとうございました!
一緒にTypeScriptを学びたい方はぜひ下記のアカウントからご連絡ください!