≪ Today I learned.
RSS購読
    公開日
    タグ
    TypeScript , Mindset
    著者
    ダーシノ

    Optional Propertyの乱用は諸悪の根源

    私は、数年来のOptional Propertyアンチ、Optional Property撲滅委員会 会長を務めている。なぜそこまでOptional Propertyを嫌うのか、その理由を説明し世の中から不要な?をなくしたい。

    TL;DR

    Optional Propertyのアンチパターン

    Optional Propertyは存在しない型を生み出す

    安易にOptional Propertyを使うことで、存在しない形を生み出してしまう。

    type Item = {
      a?: string
      b?: string
      c?: string
      d?: string
    }

    たった4つのプロパティをオプショナルにするだけで、Item型から16通りの型が生まれる。

    1. {}
    2. { a }
    3. { b }
    4. { c }
    5. { d }
    6. { a, b }
    7. { a, c }
    8. { a, d }
    9. { b, c }
    10. { b, d }
    11. { c, d }
    12. { a, b, c }
    13. { a, b, d }
    14. { a, c, d }
    15. { b, c, d }
    16. { a, b, c, d }

    16通りすべてのパターンが存在するならOptional Propertyは有用である。しかし、ほとんどの場合、組み合わせは限られているのに、プログラマが手を抜くためだけにOptional Propertyが使われるケースが多い。

    Optional Propertyで意味が失われ疑心暗鬼になる

    Optional Propertyを使うと、仕様的に必ず存在する値であってもundefinedのチェックをしたり、Optional Chainingを追加したりが必要になる。また、間違った共通化を行うことで、本来オブジェクトが持つ意味が失われ「本当にプロパティは存在するのか?」という疑心暗鬼になり、生産性が落ちる。

    たとえば、以下のような型があるとする。

    type User = {
      role: 'admin' | 'guest' | 'user'
      name: string
      /** role: 'guest'のときに有効 */
      expirationDate?: Date
      /** role: 'user'のときに有効 */
      lastLogin?: Date
    }
    
    const user: User = fetchUser()
    
    if (user.role === 'admin') {
      console.log(user.expirationDate?.toLocaleString()) // undefined
      console.log(user.lastLogin?.toLocaleString()) // undefined
    }
    if (user.role === 'guest') {
      console.log(user.expirationDate?.toLocaleString()) // Date
      console.log(user.lastLogin?.toLocaleString()) // undefined
    }
    if (user.role === 'user') {
      console.log(user.expirationDate?.toLocaleString()) // undefined
      console.log(user.lastLogin?.toLocaleString()) // Date
    }

    必ずプロパティがあるとわかっていてもOptional Chainingを書いたり、必ずundefinedになるとわかっていてもコード上ではそれが表現されない。

    Optional Propertyがコードを複雑にする

    先述の例を見ていただいたとおり、Optional Propertyのせいでコードがわかりづらくなったことがわかると思う。

    これがネストしたオブジェクトで、複数階層でOptional Propertyが使われていると手に負えない。

    Optional Propertyの代わりにUnion Typeを使おう

    先ほどのUserをUnion Typeを使ってリファクタリングしてみよう。

    type Admin = {
      role: 'admin'
      name: string
    }
    type Guest = {
      role: 'guest'
      name: string
      expirationDate: Date
    }
    type RegularUser = {
      role: 'user'
      name: string
      lastLogin: Date
    }
    
    type User = Admin | Guest | RegularUser
    
    const user: User = fetchUser()
    
    if (user.role === 'admin') {
      console.log(user.name)
      // 静的型チェックでexpirationDate, lastLoginが存在しないことが保証される
    }
    if (user.role === 'guest') {
      console.log(user.name)
      console.log(user.expirationDate.toLocaleString())
      // 静的型チェックでlastLoginが存在しないことが保証される
    }
    if (user.role === 'user') {
      console.log(user.name)
      console.log(user.lastLogin.toLocaleString())
      // 静的型チェックでexpirationDateが存在しないことが保証される
    }

    Union Typeで存在する型だけを宣言することで、コード中はroleをチェックするだけでナローイングによって正確な型がわかる。またOptional Chainingでは失われていた各型の意味が明示的に表現できるようになった。

    まとめ

    手抜きせず仕様と向き合いドメインモデルを正確に表現しよう。楽だからという理由でOptional Propertyを使うと、早ければ数カ月後には牙を剥いてくる。数年後には仕様がわからないから安全のためにとりあえずOptional Chainingがコード上に大量に現れる。

    このことが理解できないなら、Optional Propertyは使うべきではない。