Conditional Types I/O

5d9cd19df0e91caac118b793b4f803d5?s=47 Takepepe
August 23, 2018

Conditional Types I/O

Roppongi.js #5 発表資料。redux-aggregate の 型合成テクニック https://roppongi-js.connpass.com/event/95936/

5d9cd19df0e91caac118b793b4f803d5?s=128

Takepepe

August 23, 2018
Tweet

Transcript

  1. Conditional Types I/O Roppongi.js #5

  2. 自己紹介 - @Takepepe / Takefumi.Yoshii / DeNA DEG - 状態管理全般の話題が好き

    - 最近は TypeScript に夢中
  3. The Redux Action world const initialState = { count: 0

    } const types = { INCREMENT: 'COUNTER_INCREMENT' } const increment = () => ({ type: types.INCREMENT }) function reducer(state = initialState, action) { switch (action.type) { case types.INCREMENT: return { ...state, count: state.count + 1 } default: return state } }
  4. The Redux Action world const initialState = { count: 0

    } const types = { INCREMENT: 'COUNTER_INCREMENT' } const increment = () => ({ type: types.INCREMENT }) function reducer(state = initialState, action) { switch (action.type) { case types.INCREMENT: return { ...state, count: state.count + 1 } default: return state } } Action を軸足に置くRedux。 Mutation は Action に順応する。
  5. The Redux Action world const initialState = { count: 0

    } const types = { INCREMENT: 'COUNTER_INCREMENT' } const increment = () => ({ type: types.INCREMENT }) function reducer(state = initialState, action) { switch (action.type) { case types.INCREMENT: return { ...state, count: state.count + 1 } default: return state } } Action を軸足に置くRedux。 Mutation は Action に順応する。
  6. Doubt to Reducer ▪ Action 選定 / State 変更 の責務が混在している…?

    ▪ Action は最初から 複・Reducer の関心事なのか…? ▪ Action を除くとどうなる…?
  7. The State world const initialState = { count: 0 }

    function increment(state = initialState) { return { ...state, count: state.count + 1 } } State を軸足に置くRedux。
  8. The State world const initialState = { count: 0 }

    function increment(state = initialState) { return { ...state, count: state.count + 1 } } function setCount(state = initialState, { amount }) { return { ...state, count: amount } } State を軸足に置くRedux。 State 変更の責務のみが残る。
  9. The State world const initialState = { count: 0 }

    function increment(state = initialState) { return { ...state, count: state.count + 1 } } function setCount(state = initialState, { amount }) { return { ...state, count: amount } } export const Mutations = { increment, setCount } State を軸足に置くRedux。 Action は Mutation から「生成する」
  10. redux-aggregate

  11. createAggregate() import { createAggregate } from 'redux-aggregate' import { Mutations

    } from './models/counter' const { types, // Generated ActionTypes creators, // Generated ActionCreators reducerFactory // Generated ReducerFactory } = createAggregate(Mutations, 'counter/') 状態変更関数 (Mutaion) MapObject から Redux の定型句を生成。 Action の初期コンテキストは狭い。Action / Reducer 密結合スタート。
  12. createActions() import { createActions } from 'redux-aggregate' import { ActionSources

    } from './actions/timer' const { types, // Generated ActionTypes creators // Generated ActionCreators } = createActions(ActionSources, 'timer/') 純関数(ActionSrc)MapObject から Actions を生成。 Aggregate が抱えるには不自然な Action は Actions に委譲。 Action / Reducer 分割定義の余地を。
  13. redux-aggregate pros ▪ 開発速度の向上 ▪ Action 選定 / State 変更

    の責務に境界が生まれる ▪ テストが容易・可読性の向上 ▪ many-to-many の余地もある 詳細は redux-aggregate.js.org で
  14. The State world const initialState = { count: 0 }

    function increment(state = initialState) { return { ...state, count: state.count + 1 } } 何はともあれ、これだけで3種の定型句が生成されるので 開発効率が良い。型との相性も良さそうに見えるが…
  15. 型も一緒に生成

  16. createAggregate() function setCount(state: State, payload: { amount: number }) {

    // Infer Src A return { ...state, count: payload.amount } }
  17. createAggregate() function setCount(state: State, payload: { amount: number }) {

    // Infer Src A return { ...state, count: payload.amount } } const { creators } = createAggregate({ setCount }, 'counter/')
  18. createAggregate() function setCount(state: State, payload: { amount: number }) {

    // Infer Src A return { ...state, count: payload.amount } } const { creators } = createAggregate({ setCount }, 'counter/') const { type, payload } = creators.setCount({ amount: 10 }) // Inferred Dist A
  19. createAggregate() function setCount(state: State, payload: { amount: number }) {

    // Infer Src A return { ...state, count: payload.amount } } const { creators } = createAggregate({ setCount }, 'counter/') const { type, payload } = creators.setCount({ amount: 10 }) // Inferred Dist A { type: "counter/setCount", payload: { amount: 10 } }: { type: string, payload: { amount: number } // Inferred Dist A }
  20. 型がマッピングされている

  21. createActions() function tick({ message }: { message: string }) {

    // Infer Src A return { date: new Date(), message } // Infer Src B }
  22. createActions() function tick({ message }: { message: string }) {

    // Infer Src A return { date: new Date(), message } // Infer Src B } const { creators } = createActions({ tick }, 'timer/')
  23. createActions() function tick({ message }: { message: string }) {

    // Infer Src A return { date: new Date(), message } // Infer Src B } const { creators } = createActions({ tick }, 'timer/') const { type, payload } = creators.tick({ message: 'hello' }) // Inferred Dist A
  24. createActions() function tick({ message }: { message: string }) {

    // Infer Src A return { date: new Date(), message } // Infer Src B } const { creators } = createActions({ tick }, 'timer/') const { type, payload } = creators.tick({ message: 'hello' }) // Inferred Dist A { type: "timer/tick", payload: { date: XXXX, message: "hello" } }: { type: string, payload: { date: Date, message: string } // Inferred Dist B }
  25. 開発時 の 型定義 は inline assertion のみ

  26. ※ これ以降の話はライブラリ利用時 意識する必要はないです。 裏側の話。

  27. Conditional Types I/O

  28. Bottom up Generics ▪ Generics による型定義も各々の責務を明瞭に ▪ ボトムアップで要件を定義していく ▪ 同じ定義でも使い所によって振る舞いが変わる

    以降 <INPUT> は同一 mutation 関数を指す
  29. Return type type R<INPUT> = INPUT extends (...arg:[]) => infer

    OUTPUT ? OUTPUT : never // javascript 翻訳 function _R(input = () => {}) { return input() } 戻り型を抽出する型
  30. Argument type type A1<INPUT> = INPUT extends (a1: infer OUTPUT)

    => any ? OUTPUT : never type A2<INPUT> = INPUT extends (a1: any, a2: infer OUTPUT) => any ? OUTPUT : never // javascript 翻訳 function _A1 (input = a1 => a1) { return input() } function _A2 (input = (a1, a2) => a2) { return input() } n番目引数型を抽出する型
  31. Expected type type NoPayload<INPUT> = (state: A1<INPUT>) => A1<INPUT> type

    WithPayload<INPUT> = (state: A1<INPUT>, payload: A2<INPUT>) => A1<INPUT> type ExpectedType<INPUT> = NoPayload<INPUT> | WithPayload<INPUT> // javascript 翻訳 function _ExpectedType(state, payload) { if (payload === undefined) return { ...state } return { ...state, payload } } 第1引数型 / 戻り型の同一性を要求。INPUT期待型
  32. Cast type type NoPayload<INPUT> = () => { type: string

    } type WithPayload<INPUT> = (payload: A2<INPUT>) => { type: string, paylod: A2<INPUT> } type CastType<INPUT> = NoPayload<INPUT> | WithPayload<INPUT> // javascript 翻訳 function _CastType(input) { const a2 = _A2(input)  if (a2 === undefined) return () => ({ type: 'string' }) return (payload = a2) => ({ type: 'string', payload }) } 抽出した部分型を合成。OUTPUT振付型
  33. Assert type type AssertType<INPUT> = INPUT extends ExpectedType<INPUT>                ?

    CastType<INPUT>                : never // javascript 翻訳 function _AssertType(input) { if (input <= _ExpectedType(input)) { // 意訳 return _CastType(input) } } INPUT期待型 ならば OUTPUT振付型を返す。宣言型
  34. Assert by Generics function foo<INPUT>(input) { return _AssertType(input) as AssertType<INPUT>

    // Assertion } foo 関数に与えた input 関数が型制約を満たしている場合、 型が振付けられた関数が return される。 プログラマブルに型が導出できる。
  35. Mapped types & readonly wrap type AssertMap<T> = { readonly

    [K in keyof T]: AssertType<T[K]> } Mapped types & Lookup types で Assert type 仕上げ。 redux-aggregate の Mutations から生成される ActionCreators の型はこうして生まれる。
  36. Conditional Types I/O 要約

  37. Conditional Types を活用した Auto mixed subtyping by Generics (合成派生型の自動導出)テクニック

  38. ライブラリ提供関数に hook しているので、 利用者は敷居高めの型定義を気にせず 開発に集中出来る。

  39. Appendix

  40. Optional wrap breaks KeysDiff ... type SubscribeActionsMap<T, M> = {

    [K in keyof T & keyof M]?: SubscribeActions<T[K], M[K]> } 2つの MapObject を与えると、同名関数に呼応する機能がある。Optional wrap をすると、2者間の関数名 Diff が拾えない。
  41. KeysDiff type alias type KeysDiff< T extends string | number

    | symbol, U extends string | number | symbol > = ({ [P in T]: P } & { [P in U]: never } & { [x: string]: never })[T] 2者間の KeysDIff を抽出して、 Diff がなければ never に倒す型
  42. Bool by KeysDiff type alias type HasKeysDiff<T, U> = KeysDiff<keyof

    U, keyof T> extends never ? false : KeysDiff<keyof T, keyof U> extends never ? false : true KeysDiff の有無を真偽値で返す型
  43. Optional wrap & Typo guard type SubscribeActionsMap<T, M> = HasKeysDiff<T,

    M> extends false ? { [K in keyof T & keyof M]?: SubscribeActions<T[K], M[K]> } : never Optional wrap 適用前に KeysDiff でタイポチェックする。
  44. String literal type as TypeError type SubscribeActionsMap<T, M> = HasKeysDiff<T,

    M> extends false ? { [K in keyof T & keyof M]?: SubscribeActions<T[K], M[K]> } : 'SUBSCRIPTIONS_KEY_NOT_MATCH' 単純に never に倒すと undefined型になり、 どこが型違反しているのか分かりづらい。 String literal type な Error message を仕込むと利用者に優しい。
  45. Finally, Don’t believe type... 型は簡単に嘘をつくのでね…

  46. Thanks !