TSLab【第3回 TypeScript勉強会】型の絞り込みと型安全!

TSLab
  • URLをコピーしました!

こんにちは!RYOTAです!

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

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

目次

はじめに

私が運営しているTSLabの第3回定例会を実施いたしました。

今回も多くの方にご参加いただき感謝…感謝!!!

今回のテーマは Narrowing !これまで同様TypeScriptの公式ドキュメントをベースにディスカッションを実施しました!

前回は型の種類や定義の仕方についての理解を深めましたが、今回は型の判定と絞り込みについて議論を深める会となってます。

下記が前回の勉強会内容となりますので気になる方は合わせてご覧くださいませ。

あわせて読みたい
TSLab【第2回 TypeScript勉強会】今回のテーマは型定義の基礎! こんにちは!RYOTAです! 当記事をご覧いただきありがとうございます! 当記事は定例になりましたTSLab勉強会のレポート記事になります! 【はじめに】 私が運営してい...

ディスカッション内容

Narrowing(ナローイング)とは?

ナローイングとは定義した型に沿って絞り込みを行い特定の型に対して安全に処理を行うこと。

型が確定していない要素に対してのアクセスやメソッド実行をしてしまうとエラーの原因になってしまいますが、事前に型を確定しておくことで安全に処理を行うことが可能になります。

TypeScriptは静的型チェックを行い実行前に型を認識してくれるので、未定義の要素にアクセスする可能性を減らしてくれます。これを利用して型を絞り込んでいくことで安全性の高い処理をすることが可能になります。

typeof type guards

typeof によりプリミティブ型を絞り込むことが可能になります。

プリミティブ型の一覧

  • string
  • number
  • bigint
  • boolean
  • symbol
  • undefined
  • object
  • function

例えばこのようなケースの場合、渡される引数の型が確定していないので型によっては実行できないケースが存在します。

type Test = string | number

const testFunction = (test: Test) => {
  // エラー: プロパティ 'toUpperCase' は型 'Test' に存在しません。
  // エラー: プロパティ 'toUpperCase' は型 'number' に存在しません。
  return test.toUpperCase()

  // 引数testはTest型のためstringかnumberが確定していない
  // stringの場合な問題なく処理できるが、numberの場合は.toUpperCaseを実行できない
}

このようなケースではtypeofで型を確定してあげることで安全に処理をすることが可能です。(当然switch文で分岐させることも可能)

type Test = string | number

const testFunction = (test: Test) => {
  if (typeof test === 'string') {
    // 文字列であることが確定するのでtoUpperCaseが実行可能
    return test.toUpperCase()
  }

  if (typeof test === 'number') {
    // 数値であることが確定するので2乗を計算することが可能
    return test ** 2
  }
}

オプショナルプロパティと非nullアサーション

オブジェクト型のプロパティはオプショナル(無くても問題ない)にすることが可能です。

プロパティが存在すれば問題ないですが、下記のようにプロパティが存在しない場合実行時にエラーが発生しています。(Object is possibly ‘undefined’.)

interface Test {
  value?: number // オプショナルプロパティ
}

const testFunction = (test: Test) => {
  return test.value ** 2
}

const test = {
  // valueが存在しない場合
}

console.log(testFunction(test))

// エラー: Object is possibly 'undefined'.
// エラー箇所: return test.value ** 2

上記のエラーを防ぐためには非nullアサーションを使うことで回避することが可能です。

interface Test {
  value?: number // オプショナルプロパティ
}

const testFunction = (test: Test) => {
  return test.value! ** 2 // 非nullアサーションを使う
}

const test = {
  // valueが存在しない場合
}

console.log(testFunction(test))
//  -> NaN

エラーが出ずに実行できますがtest.value!がNaNになっていることが確認できます。

NaNが返ってきてる時点で処理として問題がありますが、処理落ちすることは回避できました。

逆に一定のエラーを握りつぶせてしまうので利用する場合は注意が必要となります。

等価と厳密等価

JavaScriptには等価演算時(==)と厳密等価演算子(===)があります。

文字通り値が一致しているかを判定する演算子になっていますが、=が一つ違うだけで全く違う挙動をします。

結論から言ってしまうと、基本的には厳密等価演算子(===)または、厳密不等価演算子(!==)を使いましょう。

一例ですが下記のように実際に取得できる値が異なるので、厳格チェックをしないと不測のバグの原因になることを認識しましょう。

console.log(null == undefined)
// -> true
console.log(null === undefined)
//  -> false

voidとnever

この内容は私自身も理解しきれていませんが、あえて言語化するとこんな感じでしょうか。

voidneverの違い

  • void: 処理が正常終了するが、返り値を返さない
  • never: 処理が最後まで走らず、結果的に値を返さない

うーん。よくわからない。。。笑

ディスカッションをする中でユースケースの話になったので例を挙げるとこんな感じ?

void

return値がない場合

const testVoidFunction = (): void => {
  console.log('テスト')
}

never

処理が最後まで進まない場合

const testNeverFunction1 = (): never => {
  throw new Error('途中で処理を中断しエラーを返す')
}

パターンチェックをして定義漏れを防ぐ場合

(Reduxのアクションクリエイターの定義でよく使う?)

例えば下記のような分岐処理がある関数があるとします。

type Test = 'typeA' | 'typeB'

const testFunction = (test: Test) => {
  switch (test) {
    case 'typeA':
      console.log('Aタイプです')
      break
    case 'typeB':
      console.log('Bタイプです')
      break
  }
}

ここまでは問題ないですが例えば他の人が変更を加えてTest型に他の型を加えた場合を想定します。

testFunctionはtypeAとtypeBに対応した処理が記述されていますが、他の型に対しての処理が漏れた場合は何も実行されません。(当たり前ですが)

type Test = 'typeA' | 'typeB' | 'typeC'
// typeC を加える

const testFunction = (test: Test) => {
  switch (test) {
    case 'typeA':
      console.log('Aタイプです')
      break
    case 'typeB':
      console.log('Bタイプです')
      break
    // typeC に対しての処理を記述し忘れる
  }
}

このように型を変更した時にその型に付随する処理漏れを防ぐためにneverが使えるようです。

type Test = 'typeA' | 'typeB' | 'testC'

const testFunction = (test: Test) => {
  switch (test) {
    case 'typeA':
      console.log('Aタイプです')
      break
    case 'typeB':
      console.log('Bタイプです')
      break

    // neverによるチェックを追加
    default:
      const _: never = test
    // エラー:  型 'string' を型 'never' に割り当てることはできません。
  }
}

パッと見わかりずらいですがneverには何も代入できないことを利用して型チェックをしているようですね。

上の処理でtypeAとtypeBでないことは確定していますが、testCの可能性を潰せていないので最後のdefaultで const _: never = test の代入をしようとすると、neverに割り当てようとするなボケ!って叱ってくれる訳ですね!

これ思いついた人凄い!!

その他

上記以外にもさまざまな議論が!

ディスカッションの一例

  • instanceof っていつ使うの?
  • 関数の戻り値、書く派?書かない派?
  • ユニオン・リテラル型の応用
  • in と is

こう見ると密度濃いですね…!

流石に全てまとめるのキツいので一旦ここまで!

さいごに

前回まででTSの特徴や型の取り扱いを学び、今回で型の絞り込みによるフロー制御を学びました!

改めてですがTypeScriptの型チェックって超優秀ですね。

ますますTSが好きになってきました。(笑)

新しく現場でも使える技術が見つかったのでどんどん理解を深めていこうと思います!

以上!最後までご覧くださりありがとうございました!

一緒にTypeScriptを学びたい方はぜひ下記のアカウントからご連絡ください!

TSLab

この記事が気に入ったら
フォローしてね!

よかったらシェアしてね!
  • URLをコピーしました!
  • URLをコピーしました!
目次