Upgrade to Pro — share decks privately, control downloads, hide ads and more …

Backbone.Model に 型をつけて剥がす - Typing to destroy Backbone.Model

2f374df92882247e5985413dad8b39b9?s=47 fsubal
September 20, 2019

Backbone.Model に 型をつけて剥がす - Typing to destroy Backbone.Model

2019年9月20日の社内勉強会の資料です

2f374df92882247e5985413dad8b39b9?s=128

fsubal

September 20, 2019
Tweet

Transcript

  1. Backbone.Model に 型をつけて剥がす Sub title pixiv Inc. subal 2019.9.20

  2. 2 誰 • pixivFACTORY • TypeScript と SVG と React、時々

    Rails • 型芸、SVG芸、hooks 芸の話ができます subal
  3. 3 型芸で負債を倒す話

  4. 4

  5. 5 pixivFACTORY BOOKS

  6. 6 pixivFACTORY BOOKS • 同人誌作成機能。グッズと並ぶ、 pixivFACTORY の二大柱 • 同人誌プラン検索/入稿画面が React

    + Redux • ただし、2015年に設計された React + Redux アプリケーション • アップデートの結果表面的な記法は新しくなっている(Babel, ES6 Classes) ◦ しかし本質的な設計は…? ◦ それに、グッズ側は TypeScript で書くのが当たり前に変わっている
  7. 7 設計上の課題 • mixin の名残がある ◦ React.createClass は流石にやめているが、constructor で this

    を lodash/extend している • Redux の store に Backbone.Model のインスタンスが入っている ◦ Immutable.js ならまだ分かるけど、Backbone.Model です ▪ まぁ私は Immutable.js も嫌いなのであったらあったで剥がしますけど • 他にもいろいろあるが、とりあえず ↑ を何とかする。まずはそれから
  8. 8 とりあえずのゴール • reducer で Backbone.Model を作らない • store に

    Backbone.Model を持たない • コンポーネントに Backbone.Model を持ち込ませない • TS化し、型のついたただの json にする
  9. 9 現状

  10. 10

  11. 11 セッターで型をキャストする 仕組み ( ActiveModel::Attributes みたいなやつ ) モデルのデフォルト値

  12. 12 プロパティとは別にメソッドっぽいのが生えるケースがある

  13. 13 たとえばここが、number と勝手に推論される状況を作りたい

  14. 14 おっとこんなところにデ フォルト値が

  15. 15 extend() の引数内の defaults から、 返り値の型を推論する

  16. 16

  17. 17

  18. 18

  19. 19

  20. 20 1個ずつ解説します みたいな型定義で倒せるのですが…

  21. 21 Model のインスタンスの型を考える • Backbone.Model のプロパティは(厳密には違うけど)だいたい keyof defaults • 仮に、extend()

    の引数を option とするとき… ◦ defaults の型は typeof option['defaults'] ◦ これを仮に P と置く ◦ すると、インスタンスの型はそれをジェネリクスに受け取る TypedModelInstance<P> と置くことができる ◦ その型は get<K extends keyof P>(key: K): K[P] というメソッドを持つはず
  22. 22 これにより、model.get('hoge') で hoge が defaults に書かれてないキーならば、 コンパイルエラーになる

  23. 23 Model のインスタンスの型を考える 2 • Backbone.Model の set は特殊で、以下の二つの記法が許容される (うげぇ…)

    ◦ model.set('page_size', 12) ◦ model.set({ page_size: 12 }) • TypeScript にはメソッドの型をオーバーロードする仕組みがあるので、両対応で書くことは 可能
  24. 24

  25. 25 Model のインスタンスの型を考える 3 • toJSON の型は、少なくとも推論されたすべてのプロパティを含む型を返すはず ◦ さっきの言葉で言えば、とりあえず P

    あるいは P を継承した型であるはず ◦ 実は、Backbone.Model はプロパティとは別に getters という仕組みがあり、本当は それらも toJSON に含まれる ▪ が、一旦無視してすすめる
  26. 26

  27. 27 クラスの型を考える • Backbone.Model.extend() は「クラスを作る関数」 ◦ なので、返り値がクラス(あるいは newable なオブジェクト)と考える必要がある ◦

    TypeScript は、任意の型に対して、それを new で呼んだときの返り値を定義するこ とができる ◦ Book クラスは ReturnType<typeof Backbone.Model.extend({ ... })> のような方法 で推論されるし、インスタンスの型は new の型定義から推論される
  28. 28

  29. 29 ここまでやると、 コンパイラは Book という「クラス」がどうい う構造かを理解し始める

  30. 30 Reducer から剥がしていく • model クラスをやめてただの JSON にしたあとで、返り値の型が変わらないことを保証した い ◦

    Jest のスナップショットテストを使う • 剥がした後に残る JSON の型を、interface State として書いておきたい ◦ まぁ infer を使って出てきた結果をコピーするとか … ◦ 推論結果を参考に頑張って書くしかない( optional なキーとか推論しきれてない箇所 はあるので )
  31. 31 テストのコツ • もともと、backbone.model を生で使ったテストは書かれていたが、これは使わないことに した ◦ 剥がした後も有効であり続けるテストでないと意味がない ◦ toJSON

    した結果をスナップショットテストして、 ▪ それが剥がした後も通る ▪ あるいは、落ちるけど差分が意図したものであることを検証したい
  32. 32

  33. 33

  34. 34 • props にまで Backbone のモデルインスタンスが渡っている箇所がある • コンポーネントの外に追い出したい • reducer

    にしか現れない状況を作った上で、reducer を倒すみたいな感じにしたい コンポーネントに露出している model
  35. 35 toJSON するだけの selector を書く

  36. 36 useSelector を使って末端から型付け

  37. 37 • 一部の画面 / model だけしか置き換えてないのでまだまだ • 終わったらあの型定義はちゃんと捨てる ◦ 登りきった後は梯子を投げ捨てなければならない

    今後
  38. 38 戦いはまだまだこれから…!

  39. 39 • とりあえず、全部ではないが Backbone.Model を剥がしながら型付け・モダン化が進ん でいる • TypeScript の型システムは無限に便利なのであーいうものでも型がつけられてしまう •

    スナップショットテストはライブラリを消すときの強い味方 • useSelector は最高 まとめ
  40. 40 • 一つには優先度の問題 ◦ 「他の改修を一切留めてやるべきか」といういつもの話 • もう一つは、実はこっちのほうが事故らないかもという考え ◦ 古くて型のないコードを「読解して」書き直すのと、古いコードの実装から型を「 推論さ

    せて」安全にするアプローチ ▪ 前者(書き直し)のほうが良いと自信を持って言えたか?うーん … ▪ 真面目に読むの嫌なのでコンパイラにやらせるは一つの方法としてある Q. Why not フルスクラッチ?
  41. Backbone.Model に 型をつけて剥がす Sub title pixiv Inc. subal 2019.9.20