Upgrade to Pro
— share decks privately, control downloads, hide ads and more …
Speaker Deck
Features
Speaker Deck
PRO
Sign in
Sign up for free
Search
Search
Conditional Types I/O
Search
Takepepe
August 23, 2018
Technology
1
910
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
ServerAction で Progressive Enhancement はどこまで頑張れるか? / progressive-enhancement-with-server-action
takefumiyoshii
6
460
App Router への移行は「改善」となり得るのか?/ Can migration to App Router be an improvement
takefumiyoshii
8
2.2k
フロントエンドの書くべきだったテスト、書かなくてよかったテスト
takefumiyoshii
36
14k
Webフロントエンドのための実践「テスト」手法 CodeZine Night #1
takefumiyoshii
24
7.8k
Next.js でリアーキテクトした話 / story-of-re-architect-with-nextjs
takefumiyoshii
12
8.4k
より速い WEB を目指す Next.js / nextjs-make-the-web-faster
takefumiyoshii
54
19k
フロントエンドの複雑さに耐えるため実践したこと / readyfor-nextjs-first
takefumiyoshii
25
10k
Redux の利点を振り返る
takefumiyoshii
26
8.4k
Type-only Migrate by AST
takefumiyoshii
1
590
Other Decks in Technology
See All in Technology
Building a RAG-poweredAI chat appwith Python and VS Code
pamelafox
0
170
One engineer company with Ruby on Rails
rstankov
2
460
KubeConにproposalを送りたい人へのアドバイス
sat
PRO
3
280
認知症フレンドリーテックとスタックチャン
naokiuc
0
350
require(ESM)とECMAScript仕様
uhyo
4
1k
競技としてのKaggle、役に立つKaggle
yu4u
7
2.5k
uvを使ってストレスフリーな Python開発をしよう!
r74tech
0
250
Introduction to SwiftUI V2
pohjus
0
100
NewSQL Landscape
oracle4engineer
PRO
0
170
ルーターでプレゼンする
puhitaku
1
3.4k
Autonomous Database Cloud 技術詳細 / adb-s_technical_detail_jp
oracle4engineer
PRO
15
35k
VSCodeの拡張機能を作っている話
ebarakazuhiro
1
840
Featured
See All Featured
Why Our Code Smells
bkeepers
PRO
331
56k
The Power of CSS Pseudo Elements
geoffreycrofte
62
5k
Bootstrapping a Software Product
garrettdimon
PRO
302
110k
Agile that works and the tools we love
rasmusluckow
325
20k
Imperfection Machines: The Place of Print at Facebook
scottboms
261
12k
How GitHub (no longer) Works
holman
305
140k
Build your cross-platform service in a week with App Engine
jlugia
226
17k
Clear Off the Table
cherdarchuk
85
310k
The Cult of Friendly URLs
andyhume
74
5.7k
How To Stay Up To Date on Web Technology
chriscoyier
782
250k
Refactoring Trust on Your Teams (GOTO; Chicago 2020)
rmw
26
2.3k
GraphQLの誤解/rethinking-graphql
sonatard
56
9.3k
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 !