Slide 1

Slide 1 text

Conditional Types I/O Roppongi.js #5

Slide 2

Slide 2 text

自己紹介 - @Takepepe / Takefumi.Yoshii / DeNA DEG - 状態管理全般の話題が好き - 最近は TypeScript に夢中

Slide 3

Slide 3 text

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 } }

Slide 4

Slide 4 text

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 に順応する。

Slide 5

Slide 5 text

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 に順応する。

Slide 6

Slide 6 text

Doubt to Reducer ■ Action 選定 / State 変更 の責務が混在している…? ■ Action は最初から 複・Reducer の関心事なのか…? ■ Action を除くとどうなる…?

Slide 7

Slide 7 text

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

Slide 8

Slide 8 text

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 変更の責務のみが残る。

Slide 9

Slide 9 text

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 から「生成する」

Slide 10

Slide 10 text

redux-aggregate

Slide 11

Slide 11 text

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 密結合スタート。

Slide 12

Slide 12 text

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 分割定義の余地を。

Slide 13

Slide 13 text

redux-aggregate pros ■ 開発速度の向上 ■ Action 選定 / State 変更 の責務に境界が生まれる ■ テストが容易・可読性の向上 ■ many-to-many の余地もある 詳細は redux-aggregate.js.org で

Slide 14

Slide 14 text

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

Slide 15

Slide 15 text

型も一緒に生成

Slide 16

Slide 16 text

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

Slide 17

Slide 17 text

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

Slide 18

Slide 18 text

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

Slide 19

Slide 19 text

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 }

Slide 20

Slide 20 text

型がマッピングされている

Slide 21

Slide 21 text

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

Slide 22

Slide 22 text

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

Slide 23

Slide 23 text

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

Slide 24

Slide 24 text

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 }

Slide 25

Slide 25 text

開発時 の 型定義 は inline assertion のみ

Slide 26

Slide 26 text

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

Slide 27

Slide 27 text

Conditional Types I/O

Slide 28

Slide 28 text

Bottom up Generics ■ Generics による型定義も各々の責務を明瞭に ■ ボトムアップで要件を定義していく ■ 同じ定義でも使い所によって振る舞いが変わる 以降 は同一 mutation 関数を指す

Slide 29

Slide 29 text

Return type type R = INPUT extends (...arg:[]) => infer OUTPUT ? OUTPUT : never // javascript 翻訳 function _R(input = () => {}) { return input() } 戻り型を抽出する型

Slide 30

Slide 30 text

Argument type type A1 = INPUT extends (a1: infer OUTPUT) => any ? OUTPUT : never type A2 = 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番目引数型を抽出する型

Slide 31

Slide 31 text

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

Slide 32

Slide 32 text

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

Slide 33

Slide 33 text

Assert type type AssertType = INPUT extends ExpectedType                ? CastType                : never // javascript 翻訳 function _AssertType(input) { if (input <= _ExpectedType(input)) { // 意訳 return _CastType(input) } } INPUT期待型 ならば OUTPUT振付型を返す。宣言型

Slide 34

Slide 34 text

Assert by Generics function foo(input) { return _AssertType(input) as AssertType // Assertion } foo 関数に与えた input 関数が型制約を満たしている場合、 型が振付けられた関数が return される。 プログラマブルに型が導出できる。

Slide 35

Slide 35 text

Mapped types & readonly wrap type AssertMap = { readonly [K in keyof T]: AssertType } Mapped types & Lookup types で Assert type 仕上げ。 redux-aggregate の Mutations から生成される ActionCreators の型はこうして生まれる。

Slide 36

Slide 36 text

Conditional Types I/O 要約

Slide 37

Slide 37 text

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

Slide 38

Slide 38 text

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

Slide 39

Slide 39 text

Appendix

Slide 40

Slide 40 text

Optional wrap breaks KeysDiff ... type SubscribeActionsMap = { [K in keyof T & keyof M]?: SubscribeActions } 2つの MapObject を与えると、同名関数に呼応する機能がある。Optional wrap をすると、2者間の関数名 Diff が拾えない。

Slide 41

Slide 41 text

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 に倒す型

Slide 42

Slide 42 text

Bool by KeysDiff type alias type HasKeysDiff = KeysDiff extends never ? false : KeysDiff extends never ? false : true KeysDiff の有無を真偽値で返す型

Slide 43

Slide 43 text

Optional wrap & Typo guard type SubscribeActionsMap = HasKeysDiff extends false ? { [K in keyof T & keyof M]?: SubscribeActions } : never Optional wrap 適用前に KeysDiff でタイポチェックする。

Slide 44

Slide 44 text

String literal type as TypeError type SubscribeActionsMap = HasKeysDiff extends false ? { [K in keyof T & keyof M]?: SubscribeActions } : 'SUBSCRIPTIONS_KEY_NOT_MATCH' 単純に never に倒すと undefined型になり、 どこが型違反しているのか分かりづらい。 String literal type な Error message を仕込むと利用者に優しい。

Slide 45

Slide 45 text

Finally, Don’t believe type... 型は簡単に嘘をつくのでね…

Slide 46

Slide 46 text

Thanks !