Upgrade to Pro
— share decks privately, control downloads, hide ads and more …
Speaker Deck
Speaker Deck
PRO
Sign in
Sign up
for free
Conditional Types I/O
Takepepe
August 23, 2018
Technology
1
530
Conditional Types I/O
Roppongi.js #5 発表資料。redux-aggregate の 型合成テクニック
https://roppongi-js.connpass.com/event/95936/
Takepepe
August 23, 2018
Tweet
Share
More Decks by Takepepe
See All by Takepepe
takefumiyoshii
18
6.2k
takefumiyoshii
22
5.8k
takefumiyoshii
1
310
takefumiyoshii
1
150
takefumiyoshii
2
1.1k
takefumiyoshii
6
1.6k
takefumiyoshii
15
10k
takefumiyoshii
67
17k
takefumiyoshii
2
1.4k
Other Decks in Technology
See All in Technology
karabish
0
170
viva_tweet_x
1
350
myajiri
0
300
tomihisa
1
1.1k
lmi
2
790
willnet
12
3.6k
subroh0508
4
220
adhorn
0
340
yshr1200
0
170
futo23
1
260
1stship
0
230
pg0084
1
130
Featured
See All Featured
erikaheidi
14
4.3k
schacon
145
6.6k
jeffersonlam
329
15k
hursman
107
9.2k
ddemaree
273
31k
smashingmag
283
47k
chriscoyier
684
180k
chriscoyier
499
130k
keathley
20
700
phodgson
87
3.9k
rocio
155
11k
lara
172
9.6k
Transcript
Conditional Types I/O Roppongi.js #5
自己紹介 - @Takepepe / Takefumi.Yoshii / DeNA DEG - 状態管理全般の話題が好き
- 最近は TypeScript に夢中
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 } }
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 に順応する。
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 に順応する。
Doubt to Reducer ▪ Action 選定 / State 変更 の責務が混在している…?
▪ Action は最初から 複・Reducer の関心事なのか…? ▪ Action を除くとどうなる…?
The State world const initialState = { count: 0 }
function increment(state = initialState) { return { ...state, count: state.count + 1 } } State を軸足に置くRedux。
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 変更の責務のみが残る。
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 から「生成する」
redux-aggregate
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 密結合スタート。
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 分割定義の余地を。
redux-aggregate pros ▪ 開発速度の向上 ▪ Action 選定 / State 変更
の責務に境界が生まれる ▪ テストが容易・可読性の向上 ▪ many-to-many の余地もある 詳細は redux-aggregate.js.org で
The State world const initialState = { count: 0 }
function increment(state = initialState) { return { ...state, count: state.count + 1 } } 何はともあれ、これだけで3種の定型句が生成されるので 開発効率が良い。型との相性も良さそうに見えるが…
型も一緒に生成
createAggregate() function setCount(state: State, payload: { amount: number }) {
// Infer Src A return { ...state, count: payload.amount } }
createAggregate() function setCount(state: State, payload: { amount: number }) {
// Infer Src A return { ...state, count: payload.amount } } const { creators } = createAggregate({ setCount }, 'counter/')
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
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 }
型がマッピングされている
createActions() function tick({ message }: { message: string }) {
// Infer Src A return { date: new Date(), message } // Infer Src B }
createActions() function tick({ message }: { message: string }) {
// Infer Src A return { date: new Date(), message } // Infer Src B } const { creators } = createActions({ tick }, 'timer/')
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
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 }
開発時 の 型定義 は inline assertion のみ
※ これ以降の話はライブラリ利用時 意識する必要はないです。 裏側の話。
Conditional Types I/O
Bottom up Generics ▪ Generics による型定義も各々の責務を明瞭に ▪ ボトムアップで要件を定義していく ▪ 同じ定義でも使い所によって振る舞いが変わる
以降 <INPUT> は同一 mutation 関数を指す
Return type type R<INPUT> = INPUT extends (...arg:[]) => infer
OUTPUT ? OUTPUT : never // javascript 翻訳 function _R(input = () => {}) { return input() } 戻り型を抽出する型
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番目引数型を抽出する型
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期待型
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振付型
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振付型を返す。宣言型
Assert by Generics function foo<INPUT>(input) { return _AssertType(input) as AssertType<INPUT>
// Assertion } foo 関数に与えた input 関数が型制約を満たしている場合、 型が振付けられた関数が return される。 プログラマブルに型が導出できる。
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 の型はこうして生まれる。
Conditional Types I/O 要約
Conditional Types を活用した Auto mixed subtyping by Generics (合成派生型の自動導出)テクニック
ライブラリ提供関数に hook しているので、 利用者は敷居高めの型定義を気にせず 開発に集中出来る。
Appendix
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 が拾えない。
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 に倒す型
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 の有無を真偽値で返す型
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 でタイポチェックする。
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 を仕込むと利用者に優しい。
Finally, Don’t believe type... 型は簡単に嘘をつくのでね…
Thanks !