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
1.1k
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
7
1.1k
App Router への移行は「改善」となり得るのか?/ Can migration to App Router be an improvement
takefumiyoshii
8
3.6k
フロントエンドの書くべきだったテスト、書かなくてよかったテスト
takefumiyoshii
40
16k
Webフロントエンドのための実践「テスト」手法 CodeZine Night #1
takefumiyoshii
24
9.1k
Next.js でリアーキテクトした話 / story-of-re-architect-with-nextjs
takefumiyoshii
12
9k
より速い WEB を目指す Next.js / nextjs-make-the-web-faster
takefumiyoshii
54
20k
フロントエンドの複雑さに耐えるため実践したこと / readyfor-nextjs-first
takefumiyoshii
25
11k
Redux の利点を振り返る
takefumiyoshii
26
8.9k
Type-only Migrate by AST
takefumiyoshii
1
700
Other Decks in Technology
See All in Technology
Aurora DSQLはサーバーレスアーキテクチャの常識を変えるのか
iwatatomoya
1
1k
AWSで始める実践Dagster入門
kitagawaz
1
620
なぜスクラムはこうなったのか?歴史が教えてくれたこと/Shall we explore the roots of Scrum
sanogemaru
5
1.6k
RSCの時代にReactとフレームワークの境界を探る
uhyo
10
3.4k
5分でカオスエンジニアリングを分かった気になろう
pandayumi
0
240
Function Body Macros で、SwiftUI の View に Accessibility Identifier を自動付与する/Function Body Macros: Autogenerate accessibility identifiers for SwiftUI Views
miichan
2
180
2025年夏 コーディングエージェントを統べる者
nwiizo
0
170
「どこから読む?」コードとカルチャーに最速で馴染むための実践ガイド
zozotech
PRO
0
450
ブロックテーマ時代における、テーマの CSS について考える Toro_Unit / 2025.09.13 @ Shinshu WordPress Meetup
torounit
0
130
Evolución del razonamiento matemático de GPT-4.1 a GPT-5 - Data Aventura Summit 2025 & VSCode DevDays
lauchacarro
0
200
【初心者向け】ローカルLLMの色々な動かし方まとめ
aratako
7
3.5k
生成AI時代のデータ基盤設計〜ペースレイヤリングで実現する高速開発と持続性〜 / Levtech Meetup_Session_2
sansan_randd
1
150
Featured
See All Featured
Build The Right Thing And Hit Your Dates
maggiecrowley
37
2.9k
XXLCSS - How to scale CSS and keep your sanity
sugarenia
248
1.3M
Designing for humans not robots
tammielis
253
25k
Why You Should Never Use an ORM
jnunemaker
PRO
59
9.5k
Learning to Love Humans: Emotional Interface Design
aarron
273
40k
[RailsConf 2023 Opening Keynote] The Magic of Rails
eileencodes
30
9.7k
Writing Fast Ruby
sferik
628
62k
Agile that works and the tools we love
rasmusluckow
330
21k
Performance Is Good for Brains [We Love Speed 2024]
tammyeverts
12
1.1k
Testing 201, or: Great Expectations
jmmastey
45
7.7k
Let's Do A Bunch of Simple Stuff to Make Websites Faster
chriscoyier
507
140k
The Psychology of Web Performance [Beyond Tellerrand 2023]
tammyeverts
49
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 !