Slide 1

Slide 1 text

TypeScript CompilerAPI による Vuex の参照型生成 Vue.js for 2020 @Takepepe

Slide 2

Slide 2 text

About Me ■ Takefumi Yoshii / @Takepepe ■ DeNA / DeSC Healthcare ■ Frontend Engineer ■ TypeScript Meetup JP member 2

Slide 3

Slide 3 text

Agenda ■ 1. Vuex型推論への挑戦 ■ 2. 辿りついた一つの解法 ■ 3. 導出の章 ■ 4. 創出の章 3

Slide 4

Slide 4 text

1. Vuex型推論への挑戦 Challenge to Vuex type type inference

Slide 5

Slide 5 text

1-1. 型推論への多角的アプローチ

Slide 6

Slide 6 text

型推論へのアプローチは、 様々なプログラミングと同じく、 ひとつではありません。 これは、TypeScript だからこそ 明言できる言葉です。 1-1. 型推論への多角的アプローチ 6 6

Slide 7

Slide 7 text

TypeScript は柔軟であり、 どんなコンテキストにも応えてくれる 寛容な型システムを保有しています。 1-1. 型推論への多角的アプローチ 7 7 7

Slide 8

Slide 8 text

「緩くはじめ・次第に厳格に」 というアプローチも可能なため、 多くの開発者に支持されました。 1-1. 型推論への多角的アプローチ 8 8 8

Slide 9

Slide 9 text

今日発表する内容は「緩い・厳格」 というベクトルで測るならば、 最も厳格なものに位置します。 1-1. 型推論への多角的アプローチ 9 9 9

Slide 10

Slide 10 text

1-1. 型推論への多角的アプローチ 厳格にするため、あらゆる手段を導入しています。 数ある選択肢のうち一つとして、 ご周知いただければ幸いです。 10 10 10

Slide 11

Slide 11 text

1-2. あるべき型とライブラリの姿を目指して

Slide 12

Slide 12 text

1-2. あるべき型とライブラリの姿を目指して 本日とりあげる Vuex は言わずもがな、 TypeScript に最適化するために、 いくつかの困難が伴います。 12 12 12

Slide 13

Slide 13 text

1-2. あるべき型とライブラリの姿を目指して その立ちはだかる困難から、 TypeScript が嫌いになったり、 Vuexを採用しない、とされた方も 少なくないでしょう。 13 13 13

Slide 14

Slide 14 text

1-2. あるべき型とライブラリの姿を目指して 「型の課題は型で解決する」これが叶ったとき 「ライブラリ・型システム双方の良さ」が 取り戻せるはずです。 14 14 14

Slide 15

Slide 15 text

1-2. あるべき型とライブラリの姿を目指して それは、誰しもが求めていた最適解であり、 どんなライブラリであっても 目指すべきゴールだと私は考えます。 15 15 15

Slide 16

Slide 16 text

1-3. 現在のソリューション

Slide 17

Slide 17 text

1-3. 現在のソリューション 通常のアプローチでは型推論が叶わない現状から、 コミュニティから様々なソリューションが 提案されました。 17 17 17

Slide 18

Slide 18 text

1-3. 現在のソリューション それらのソリューションには 「ランタイムへの介入」という 「型都合による葛藤」がありました。 18 18 18

Slide 19

Slide 19 text

ラッパー関数・class 構文に頼ることで、 不十分な型推論を強化することはできます。 (私はこれを型プロキシと呼んでいます) 19 19 19 1-3. 現在のソリューション

Slide 20

Slide 20 text

「Vue.js で class 構文を使うのは何故ですか?」 という問いには、ほとんどが「型推論のため」 という答えが返ってくるでしょう。 20 20 20 1-3. 現在のソリューション

Slide 21

Slide 21 text

ランタイム観点において、機能的貢献のない 「型課題のためだけに施されたアプローチ」は、 本来望まれたものではありません。 21 21 21 1-3. 現在のソリューション

Slide 22

Slide 22 text

TypeScript 化することで、 一般的なコードベースから逸れることに、 抵抗を感じる声も少なからずありました。 22 22 22 1-3. 現在のソリューション

Slide 23

Slide 23 text

1-4. 不自然なコードベース

Slide 24

Slide 24 text

1-4. 不自然なコードベース 私は TypeScript に関する書籍を執筆し、 今年の6月に刊行しました。 Vue.js や Nuxt.js の型定義についても 触れている書籍です。 24 24 24

Slide 25

Slide 25 text

1-4. 不自然なコードベース 型定義のみで可能な手段を総導入し、 Vuex であっても「ランタイムへの介入」を 克服できることを、解説しています。 25 25 25

Slide 26

Slide 26 text

1-4. 不自然なコードベース そのアプローチであれば一定の型安全を、 通常のコードベースでも 実現することができます。 26 26 26

Slide 27

Slide 27 text

1-4. 不自然なコードベース しかしながら「条件を取り決め・与条件に従う」 というアプローチは、TypeScript らしい ものとは少し違いました。 27 27 27

Slide 28

Slide 28 text

1-4. 不自然なコードベース 単純な関数定義を思い出してみてください。 28 28 28 function getRank(amount?: number) { if (!amount) return null if (amount > 50) return 'A' return 'B' } const rank1 = getRank() const rank2 = getRank(50) const rank1: "A" | "B" | null const rank2: "A" | "B" | null inferred

Slide 29

Slide 29 text

1-4. 不自然なコードベース 推論器は実装内容を正しく捉え、型注釈なくして型情報は導かれます。 29 29 29 function getRank(amount?: number) { if (!amount) return null if (amount > 50) return 'A' return 'B' } const rank1 = getRank() const rank2 = getRank(50) const rank1: "A" | "B" | null const rank2: "A" | "B" | null inferred

Slide 30

Slide 30 text

1-4. 不自然なコードベース 私はこれを「TypeScriptのアイデンティティ」だと感じています。 30 30 30 function getRank(amount?: number) { if (!amount) return null if (amount > 50) return 'A' return 'B' } const rank1 = getRank() const rank2 = getRank(50) const rank1: "A" | "B" | null const rank2: "A" | "B" | null inferred

Slide 31

Slide 31 text

1-4. 不自然なコードベース 多くの型定義を必要としないコードこそ TypeScript らしいコードであり、 親しまれている「DX」だと私は考えています。 その「DX」は Vue.js や Vuex らしい、 守りたいアイデンティティです。 31 31 31

Slide 32

Slide 32 text

2. 辿りついた一つの解法 One solution I arrived at

Slide 33

Slide 33 text

葛藤と不自然さを取り除き アイデンティティを守る

Slide 34

Slide 34 text

これらの理念のもと開発したのが 「vuex-guardian」です

Slide 35

Slide 35 text

2-1. What is vuex-guardian? 「vuex-guardian」は内部で、 TypeScript CompilerAPI を 利用しています。 35 35 35

Slide 36

Slide 36 text

2-1. What is vuex-guardian? 「CompilerAPI」はさほど 新しい機能ではありませんが、 一部の開発者にしか知られていない、 「Undocumented」な機能です。 36 36 36

Slide 37

Slide 37 text

2-1. What is vuex-guardian? この「CompilerAPI」ですが、 昨今の推論機能向上との相乗効果により、 劇的な可能性が生まていることを 私は発見しました。 37 37 37

Slide 38

Slide 38 text

これは「参照型」の生成ツールです

Slide 39

Slide 39 text

「参照型」は「SourceMap File」に似たものです

Slide 40

Slide 40 text

コンパイラが知り得ないエイリアスを作成し、 欠落した型参照を補います。

Slide 41

Slide 41 text

2-2. DEMO いったいどのようなコードベースで、 どのような型推論が実現したのか? デモ動画を用意しましたので、 ご覧ください。 41 41 41

Slide 42

Slide 42 text

https://vimeo.com/365567093 2-2. DEMO

Slide 43

Slide 43 text

2-3. vuex-guardian code base

Slide 44

Slide 44 text

2-3. vuex-guardian code base 動画で紹介した コードベースを おさらいします。 44 44 44 import { LocalContext } from 'vuex' type Context = LocalContext['counter'] export const mutations = { setCount(state: State, payload: { amount: number }) { state.count = payload.amount } } export const actions = { acyncSetCount(ctx: Context, payload: { amount: number }) { ctx.commit({ type: 'setCount', amount: payload.amount }) } }

Slide 45

Slide 45 text

2-3. vuex-guardian code base はじめに、LocalContext を import 。単一 Module の Context を特定します。 45 45 45 import { LocalContext } from 'vuex' type Context = LocalContext['counter'] // Module NameSpace export const mutations = { setCount(state: State, payload: { amount: number }) { state.count = payload.amount } } export const actions = { acyncSetCount(ctx: Context, payload: { amount: number }) { ctx.commit({ type: 'setCount', amount: payload.amount }) } }

Slide 46

Slide 46 text

2-3. vuex-guardian code base これを action 関数の 引数に型注釈します。 46 46 46 import { LocalContext } from 'vuex' type Context = LocalContext['counter'] export const mutations = { setCount(state: State, payload: { amount: number }) { state.count = payload.amount } } export const actions = { acyncSetCount(ctx: Context, payload: { amount: number }) { ctx.commit({ type: 'setCount', amount: payload.amount }) } }

Slide 47

Slide 47 text

2-3. vuex-guardian code base これだけで、単一 Module 内の定義に対し、 型安全が約束されます。 47 47 47 import { LocalContext } from 'vuex' type Context = LocalContext['counter'] export const mutations = { setCount(state: State, payload: { amount: number }) { state.count = payload.amount } } export const actions = { acyncSetCount(ctx: Context, payload: { amount: number }) { ctx.commit({ type: 'setCount', amount: payload.amount }) } }

Slide 48

Slide 48 text

2-3. vuex-guardian code base mutation 関数引数を 変更したら、 コンパイルエラーを得る ことが出来ます。 48 48 48 import { LocalContext } from 'vuex' type Context = LocalContext['counter'] export const mutations = { setCount(state: State, payload: { amount: string }) { state.count = payload.amount } } export const actions = { acyncSetCount(ctx: Context, payload: { amount: number }) { ctx.commit({ type: 'setCount', amount: payload.amount }) } }

Slide 49

Slide 49 text

2-3. vuex-guardian code base 関数名の変更も、 すぐさま検知されます。 49 49 49 import { LocalContext } from 'vuex' type Context = LocalContext['counter'] export const mutations = { setValue(state: State, payload: { amount: number }) { state.count = payload.amount } } export const actions = { acyncSetCount(ctx: Context, payload: { amount: number }) { ctx.commit({ type: 'setCount', amount: payload.amount }) } }

Slide 50

Slide 50 text

2-3. vuex-guardian code base 定義することは、 これだけです。 実装を型定義の 一次ソースとしています。 50 50 50 import { LocalContext } from 'vuex' type Context = LocalContext['counter'] export const mutations = { setCount(state: State, payload: { amount: number }) { state.count = payload.amount } } export const actions = { acyncSetCount(ctx: Context, payload: { amount: number }) { ctx.commit({ type: 'setCount', amount: payload.amount }) } }

Slide 51

Slide 51 text

2-3. vuex-guardian code base 'vuex' パッケージには、 全 Store 実装の「型」 が格納されています。 51 51 51 import { LocalContext } from 'vuex' type Context = LocalContext['counter'] export const mutations = { setCount(state: State, payload: { amount: number }) { state.count = payload.amount } } export const actions = { acyncSetCount(ctx: Context, payload: { amount: number }) { ctx.commit({ type: 'setCount', amount: payload.amount }) } }

Slide 52

Slide 52 text

2-3. vuex-guardian code base 自然なコードベースのため、 TypeScript 初学者であっても、 実装に悩むことはほとんどないでしょう。 52 52 52

Slide 53

Slide 53 text

2-3. vuex-guardian code base 少し調整が必要ですが、 SFC における Vue.extend 記法であっても、 型推論は行き届きます。 53 53 53 参考:https://qiita.com/Takepepe/items/593d2d9e7dfd38eddbd4

Slide 54

Slide 54 text

2-3. vuex-guardian code base 必要なのは、このツールを バックグラウンドで起動しておくことのみです。 54 54 54 $ npx vuex-guardian

Slide 55

Slide 55 text

2-3. vuex-guardian code base 本日は多くの時間を頂いておりますので、 このツールの内側について、 概要を紹介させていただきます。 55 55 55

Slide 56

Slide 56 text

3. 導出の章 Derivation chapter

Slide 57

Slide 57 text

3-1. 導出の型パズル「Conditional Types」

Slide 58

Slide 58 text

3-1. 導出の型パズル「Conditional Types」 Conditional Types は登場以来、 プログラマブルな様から日本国内で 「型パズル」と呼ばれています。 58 58 58

Slide 59

Slide 59 text

3-1. 導出の型パズル「Conditional Types」 Type Inference in conditional types で可能になった「型の部分抽出」がなければ、 本日の発表は成立しませんでした。 59 59 59

Slide 60

Slide 60 text

3-1. 導出の型パズル「Conditional Types」 「vuex-guardian」でどの様な 型パズルが組まれているか、 解説していきます。 60 60 60

Slide 61

Slide 61 text

3-2. 型の部分抽出

Slide 62

Slide 62 text

3-2. 型の部分抽出 簡単な例を紹介します。 次の様な関数から「第二引数型だけ」抽出したい場合。 62 62 62 function addTodo(state: State, todo: Todo) { state.todos.push(todo) }

Slide 63

Slide 63 text

3-2. 型の部分抽出 簡単な例を紹介します。 次の様な関数から「第二引数型だけ」抽出したい場合。 63 63 63 function addTodo(state: State, todo: Todo) { state.todos.push(todo) }

Slide 64

Slide 64 text

3-2. 型の部分抽出 次の「A2」の様な補助型を利用すると、第二引数型が抽出できます。 64 64 64 type A2 = T extends (a1: any, a2: infer I) => any ? I : never type T = typeof addTodo type U = A2 type T = (state: State, todo: Todo) => void type U = Todo inferred

Slide 65

Slide 65 text

3-2. 型の部分抽出 ここが「Type Inference in conditional types」です。 65 65 65 type A2 = T extends (a1: any, a2: infer I) => any ? I : never type T = typeof addTodo type U = A2 type T = (state: State, todo: Todo) => void type U = Todo inferred

Slide 66

Slide 66 text

3-2. 型の部分抽出 typeof 型クエリーを使うことで、実装から型を抽出できます。 66 66 66 type A2 = T extends (a1: any, a2: infer I) => any ? I : never type T = typeof addTodo type U = A2 type T = (state: State, todo: Todo) => void type U = Todo inferred

Slide 67

Slide 67 text

3-2. 型の部分抽出 「addTodo」という、さきほどの関数定義を参照しています。 67 67 67 type A2 = T extends (a1: any, a2: infer I) => any ? I : never type T = typeof addTodo type U = A2 type T = (state: State, todo: Todo) => void type U = Todo inferred

Slide 68

Slide 68 text

3-2. 型の部分抽出 それは新しい型として定義され、参照を継続します。 68 68 68 type A2 = T extends (a1: any, a2: infer I) => any ? I : never type T = typeof addTodo type U = A2 type T = (state: State, todo: Todo) => void type U = Todo inferred

Slide 69

Slide 69 text

3-2. 型の部分抽出 実際に mutation 関数から型を抽出してみます。 69 69 69 export const mutations = { addTodo(state: State, payload: { todo: Todo }) { state.todos.push(payload.todo) } } type T = A2 type T = { todo: Todo }

Slide 70

Slide 70 text

3-2. 型の部分抽出 型定義は JavaScript と同じように、添字参照が出来ます。 70 70 70 export const mutations = { addTodo(state: State, payload: { todo: Todo }) { state.todos.push(payload.todo) } } type T = A2 type T = { todo: Todo }

Slide 71

Slide 71 text

3-2. 型の部分抽出 typeof 型クエリーと、さきほどの「A2」型を併用すると… 71 71 71 export const mutations = { addTodo(state: State, payload: { todo: Todo }) { state.todos.push(payload.todo) } } type T = A2 type T = { todo: Todo }

Slide 72

Slide 72 text

3-2. 型の部分抽出 これはまさに、store.commit に必要な payload 型です。 72 72 72 export const mutations = { addTodo(state: State, payload: { todo: Todo }) { state.todos.push(payload.todo) } } type T = A2 type T = { todo: Todo }

Slide 73

Slide 73 text

3-2. 型の部分抽出 次に、getter 関数を見ていきましょう。 73 73 73 export const getters = { doneCount(state: State) { return state.todos.filter(todo => todo.done).length } } type T = ReturnType type T = number

Slide 74

Slide 74 text

3-2. 型の部分抽出 この関数は、外部からは値として見えます。 74 74 74 export const getters = { doneCount(state: State) { return state.todos.filter(todo => todo.done).length } } type T = ReturnType type T = number this.$store.getters['todos/doneCount']

Slide 75

Slide 75 text

3-2. 型の部分抽出 これは、戻り型で表現することができます。 75 75 75 export const getters = { doneCount(state: State) { return state.todos.filter(todo => todo.done).length } } type T = ReturnType type T = number

Slide 76

Slide 76 text

3-2. 型の部分抽出 ビルトインユーティリティ型の「ReturnType」を利用すれば… 76 76 76 export const getters = { doneCount(state: State) { return state.todos.filter(todo => todo.done).length } } type T = ReturnType type T = number

Slide 77

Slide 77 text

3-2. 型の部分抽出 値に相当する型を抽出することができました。 77 77 77 export const getters = { doneCount(state: State) { return state.todos.filter(todo => todo.done).length } } type T = ReturnType type T = number

Slide 78

Slide 78 text

3-2. 型の部分抽出 ここまでの様に、ひとつひとつの 「定義済み実装」から型を抽出することで、 信頼できる「型の一次ソース」として 正しく型を参照することができます。 78 78 78

Slide 79

Slide 79 text

3-3. 型の文字列参照

Slide 80

Slide 80 text

3-3. 型の文字列参照 もうひとつ、Vuex は文字列による 「Root参照」があります。 ここまでで抽出した型を、 「Root参照文字列」にマッピングしていきます。 80 80 80

Slide 81

Slide 81 text

3-3. 型の文字列参照 Root参照文字列を見てみましょう。 81 81 81 interface RootGetters { 'todos/doneCount': LocalGetters['todos']['doneCount'] }

Slide 82

Slide 82 text

3-3. 型の文字列参照 文字列をプロパティキーとすることで、 参照文字列を表現できます。 82 82 82 interface RootGetters { 'todos/doneCount': LocalGetters['todos']['doneCount'] }

Slide 83

Slide 83 text

3-3. 型の文字列参照 そこへ、対応する参照型を格納します。 83 83 83 interface RootGetters { 'todos/doneCount': LocalGetters['todos']['doneCount'] }

Slide 84

Slide 84 text

3-3. 型の文字列参照 参照型は「一つの型定義」に集約します。 84 84 84 interface RootGetters { 'todos/doneItems': LocalGetters['todos']['doneItems'] 'todos/visibleItems': LocalGetters['todos']['visibleItems'] 'todos/doneCount': LocalGetters['todos']['doneCount'] }

Slide 85

Slide 85 text

3-3. 型の文字列参照 全ての参照は、集約されたこの型を利用します。 85 85 85 interface RootGetters { 'todos/doneItems': LocalGetters['todos']['doneItems'] 'todos/visibleItems': LocalGetters['todos']['visibleItems'] 'todos/doneCount': LocalGetters['todos']['doneCount'] }

Slide 86

Slide 86 text

3-4. 宣言空間の活用

Slide 87

Slide 87 text

3-4. 宣言空間の活用 集約した参照型は、 'vuex' 名前空間に対し Ambient Module 宣言。 87 87 87 import 'vuex' declare module 'vuex' { // Ambient Module Declaration interface RootGetters { 'counter/double': LocalGetters['counter']['double'] 'counter/expo': LocalGetters['counter']['expo'] 'todos/todosCount': LocalGetters['todos']['todosCount'] 'todos/doneCount': LocalGetters['todos']['doneCount'] } }

Slide 88

Slide 88 text

3-4. 宣言空間の活用 集約した参照型は、 'vuex' 名前空間に対し Ambient Module 宣言。 88 88 88 import 'vuex' declare module 'vuex' { interface RootGetters { 'counter/double': LocalGetters['counter']['double'] 'counter/expo': LocalGetters['counter']['expo'] 'todos/todosCount': LocalGetters['todos']['todosCount'] 'todos/doneCount': LocalGetters['todos']['doneCount'] } }

Slide 89

Slide 89 text

3-4. 宣言空間の活用 Ambient Module 宣言 によりその型定義は、 まるでライブラリに本来 備わったものである かの様に振る舞います。 89 89 89 import 'vuex' declare module 'vuex' { interface RootGetters { 'counter/double': LocalGetters['counter']['double'] 'counter/expo': LocalGetters['counter']['expo'] 'todos/todosCount': LocalGetters['todos']['todosCount'] 'todos/doneCount': LocalGetters['todos']['doneCount'] } }

Slide 90

Slide 90 text

3-4. 宣言空間の活用 vuex-guardian は、 より強固に定義された 「StrictStore型」を 持ちます。 90 90 90 import 'vuex' declare module 'vuex' { interface StrictStore extends Store { getters: RootGetters commit: StrictCommit dispatch: StrictDispatch } }

Slide 91

Slide 91 text

3-4. 宣言空間の活用 「StrictStore型」は このように宣言空間に 宣言された集約型を 参照します。 91 91 91 import 'vuex' declare module 'vuex' { interface StrictStore extends Store { getters: RootGetters commit: StrictCommit dispatch: StrictDispatch } }

Slide 92

Slide 92 text

3-4. 宣言空間の活用 この「StrictStore型」が もつ commit / dispatch について、もう少し 深掘りしていきます。 92 92 92 import 'vuex' declare module 'vuex' { interface StrictStore extends Store { getters: RootGetters commit: StrictCommit dispatch: StrictDispatch } }

Slide 93

Slide 93 text

3-5. 文字列参照と Lookup

Slide 94

Slide 94 text

3-5. 文字列参照と Lookup $store.dispatch について確認していきましょう。 94 94 94 interface ActionTypes { 'todos/acyncAddTodo': LocalActionTypes['todos']['acyncAddTodo'] 'todos/acyncDoneTodo': LocalActionTypes['todos']['acyncDoneTodo'] } interface StrictDispatch { (type: T, payload?: A[T], options?: DispatchOptions): Promise } this.$store.dispatch('todos/acyncDoneTodo', { id: this.data.id })

Slide 95

Slide 95 text

3-5. 文字列参照と Lookup 従来、文字列参照による型の特定は出来ませんでした。 95 95 95 interface ActionTypes { 'todos/acyncAddTodo': LocalActionTypes['todos']['acyncAddTodo'] 'todos/acyncDoneTodo': LocalActionTypes['todos']['acyncDoneTodo'] } interface StrictDispatch { (type: T, payload?: A[T], options?: DispatchOptions): Promise } this.$store.dispatch('todos/acyncDoneTodo', { id: this.data.id })

Slide 96

Slide 96 text

3-5. 文字列参照と Lookup 参照型が格納されたプロパティキーは、文字列です。 96 96 96 interface ActionTypes { 'todos/acyncAddTodo': LocalActionTypes['todos']['acyncAddTodo'] 'todos/acyncDoneTodo': LocalActionTypes['todos']['acyncDoneTodo'] } interface StrictDispatch { (type: T, payload?: A[T], options?: DispatchOptions): Promise } this.$store.dispatch('todos/acyncDoneTodo', { id: this.data.id })

Slide 97

Slide 97 text

3-5. 文字列参照と Lookup 格納されている参照型は、payload型に相当します。 97 97 97 interface ActionTypes { 'todos/acyncAddTodo': LocalActionTypes['todos']['acyncAddTodo'] 'todos/acyncDoneTodo': LocalActionTypes['todos']['acyncDoneTodo'] } interface StrictDispatch { (type: T, payload?: A[T], options?: DispatchOptions): Promise } this.$store.dispatch('todos/acyncDoneTodo', { id: this.data.id })

Slide 98

Slide 98 text

3-5. 文字列参照と Lookup 実は、これは強推論のために用意されたものです。 98 98 98 interface ActionTypes { 'todos/acyncAddTodo': LocalActionTypes['todos']['acyncAddTodo'] 'todos/acyncDoneTodo': LocalActionTypes['todos']['acyncDoneTodo'] } interface StrictDispatch { (type: T, payload?: A[T], options?: DispatchOptions): Promise } this.$store.dispatch('todos/acyncDoneTodo', { id: this.data.id })

Slide 99

Slide 99 text

3-5. 文字列参照と Lookup 関数は与えられた文字列から、型の Lookup が可能です。 99 99 99 interface ActionTypes { 'todos/acyncAddTodo': LocalActionTypes['todos']['acyncAddTodo'] 'todos/acyncDoneTodo': LocalActionTypes['todos']['acyncDoneTodo'] } interface StrictDispatch { (type: T, payload?: A[T], options?: DispatchOptions): Promise } this.$store.dispatch('todos/acyncDoneTodo', { id: this.data.id })

Slide 100

Slide 100 text

3-5. 文字列参照と Lookup 第一引数(ActionType文字列)が与えられた時… 100 100 100 interface ActionTypes { 'todos/acyncAddTodo': LocalActionTypes['todos']['acyncAddTodo'] 'todos/acyncDoneTodo': LocalActionTypes['todos']['acyncDoneTodo'] } interface StrictDispatch { (type: T, payload?: A[T], options?: DispatchOptions): Promise } this.$store.dispatch('todos/acyncDoneTodo', { id: this.data.id })

Slide 101

Slide 101 text

3-5. 文字列参照と Lookup それに対応する型が確定します。 101 101 101 interface ActionTypes { 'todos/acyncAddTodo': LocalActionTypes['todos']['acyncAddTodo'] 'todos/acyncDoneTodo': LocalActionTypes['todos']['acyncDoneTodo'] } interface StrictDispatch { (type: T, payload?: A[T], options?: DispatchOptions): Promise } this.$store.dispatch('todos/acyncDoneTodo', { id: this.data.id })

Slide 102

Slide 102 text

3-5. 文字列参照と Lookup もう一度。第一引数(ActionType文字列)が与えられた時… 102 102 102 interface ActionTypes { 'todos/acyncAddTodo': LocalActionTypes['todos']['acyncAddTodo'] 'todos/acyncDoneTodo': LocalActionTypes['todos']['acyncDoneTodo'] } interface StrictDispatch { (type: T, payload?: A[T], options?: DispatchOptions): Promise } this.$store.dispatch('todos/acyncDoneTodo', { id: this.data.id })

Slide 103

Slide 103 text

3-5. 文字列参照と Lookup それに対応する型が確定します。 103 103 103 interface ActionTypes { 'todos/acyncAddTodo': LocalActionTypes['todos']['acyncAddTodo'] 'todos/acyncDoneTodo': LocalActionTypes['todos']['acyncDoneTodo'] } interface StrictDispatch { (type: T, payload?: A[T], options?: DispatchOptions): Promise } this.$store.dispatch('todos/acyncDoneTodo', { id: this.data.id })

Slide 104

Slide 104 text

3-5. 文字列参照と Lookup この機能により、コードに型情報がなくとも、型安全が担保されます。 104 104 104 interface ActionTypes { 'todos/acyncAddTodo': LocalActionTypes['todos']['acyncAddTodo'] 'todos/acyncDoneTodo': LocalActionTypes['todos']['acyncDoneTodo'] } interface StrictDispatch { (type: T, payload?: A[T], options?: DispatchOptions): Promise } this.$store.dispatch('todos/acyncDoneTodo', { id: this.data.id })

Slide 105

Slide 105 text

3-5. 文字列参照と Lookup JavaScript コードと「同じ」であっても、型情報はついてまわります。 105 105 105 interface ActionTypes { 'todos/acyncAddTodo': LocalActionTypes['todos']['acyncAddTodo'] 'todos/acyncDoneTodo': LocalActionTypes['todos']['acyncDoneTodo'] } interface StrictDispatch { (type: T, payload?: A[T], options?: DispatchOptions): Promise } this.$store.dispatch('todos/acyncDoneTodo', { id: this.data.id })

Slide 106

Slide 106 text

4. 創出の章 Creation chapter

Slide 107

Slide 107 text

4-1. 創出の型パズル「Compiler API」

Slide 108

Slide 108 text

4-1. 創出の型パズル「Compiler API」 こうして全ての参照型を集約し 強化された「StrictStore型」は、 SFC の $store に適用されます。 108 108 108 interface LocalGetters { todos: { todosCount:ReturnType doneCount: ReturnType } } interface RootGetters { 'todos/todosCount': LocalGetters['todos']['todosCount'] 'todos/doneCount': LocalGetters['todos']['doneCount'] } interface MutationTypes { 'todos/addTodo': LocalMutationTypes['todos']['addTodo'] 'todos/doneTodo': LocalMutationTypes['todos']['doneTodo'] } interface ActionTypes { 'todos/acyncAddTodo': LocalActionTypes['todos']['acyncAddTodo'] 'todos/acyncDoneTodo': LocalActionTypes['todos']['acyncDoneTodo'] }

Slide 109

Slide 109 text

4-1. 創出の型パズル「Compiler API」 そのため SFC であっても、 Vuex における変化を、 コンパイルエラーとして 検出することが出来ます。 109 109 109 interface LocalGetters { todos: { todosCount:ReturnType doneCount: ReturnType } } interface RootGetters { 'todos/todosCount': LocalGetters['todos']['todosCount'] 'todos/doneCount': LocalGetters['todos']['doneCount'] } interface MutationTypes { 'todos/addTodo': LocalMutationTypes['todos']['addTodo'] 'todos/doneTodo': LocalMutationTypes['todos']['doneTodo'] } interface ActionTypes { 'todos/acyncAddTodo': LocalActionTypes['todos']['acyncAddTodo'] 'todos/acyncDoneTodo': LocalActionTypes['todos']['acyncDoneTodo'] }

Slide 110

Slide 110 text

4-1. 創出の型パズル「Compiler API」 しかし、参照型の量は膨大です。 手動でこの型を定義していくのは、 あまりにも骨が折れます。 110 110 110 interface LocalGetters { todos: { todosCount:ReturnType doneCount: ReturnType } } interface RootGetters { 'todos/todosCount': LocalGetters['todos']['todosCount'] 'todos/doneCount': LocalGetters['todos']['doneCount'] } interface MutationTypes { 'todos/addTodo': LocalMutationTypes['todos']['addTodo'] 'todos/doneTodo': LocalMutationTypes['todos']['doneTodo'] } interface ActionTypes { 'todos/acyncAddTodo': LocalActionTypes['todos']['acyncAddTodo'] 'todos/acyncDoneTodo': LocalActionTypes['todos']['acyncDoneTodo'] }

Slide 111

Slide 111 text

4-1. 創出の型パズル「Compiler API」 リファクタリングしようものなら、 多大な修正を強いられます。 これは求めたものではありません。 111 111 111 interface LocalGetters { todos: { todosCount:ReturnType doneCount: ReturnType } } interface RootGetters { 'todos/todosCount': LocalGetters['todos']['todosCount'] 'todos/doneCount': LocalGetters['todos']['doneCount'] } interface MutationTypes { 'todos/addTodo': LocalMutationTypes['todos']['addTodo'] 'todos/doneTodo': LocalMutationTypes['todos']['doneTodo'] } interface ActionTypes { 'todos/acyncAddTodo': LocalActionTypes['todos']['acyncAddTodo'] 'todos/acyncDoneTodo': LocalActionTypes['todos']['acyncDoneTodo'] }

Slide 112

Slide 112 text

4-1. 創出の型パズル「Compiler API」 この「参照型」を自動生成 するために最適な機能が 「CompilerAPI」です。 112 112 112 interface LocalGetters { todos: { todosCount:ReturnType doneCount: ReturnType } } interface RootGetters { 'todos/todosCount': LocalGetters['todos']['todosCount'] 'todos/doneCount': LocalGetters['todos']['doneCount'] } interface MutationTypes { 'todos/addTodo': LocalMutationTypes['todos']['addTodo'] 'todos/doneTodo': LocalMutationTypes['todos']['doneTodo'] } interface ActionTypes { 'todos/acyncAddTodo': LocalActionTypes['todos']['acyncAddTodo'] 'todos/acyncDoneTodo': LocalActionTypes['todos']['acyncDoneTodo'] }

Slide 113

Slide 113 text

4-2. DEMO それでは実際に、参照型が出力される デモ動画を用意しましたので、 ご覧ください。 113 113 113

Slide 114

Slide 114 text

https://vimeo.com/365568367 4-2. DEMO

Slide 115

Slide 115 text

4-3. tsc と CompilerAPI の概要

Slide 116

Slide 116 text

4-3. tsc と CompilerAPI の概要 自動生成ツールの定義の前に、 tsc と CompilerAPI について少しお話をします。 116 116 116

Slide 117

Slide 117 text

4-3. tsc と CompilerAPI の概要 tsc は TypeScript プロジェクトを JavaScript にビルドするコマンドですね。 117 117 117

Slide 118

Slide 118 text

4-3. tsc と CompilerAPI の概要 tsc ビルド時、はじめに tsconfig がパースされます。 118 118 118 tsconfig

Slide 119

Slide 119 text

4-3. tsc と CompilerAPI の概要 そこに記された設定から、対象 SRC となるファイルパス一覧を取得。 119 119 119 'path/to/modules/todo.ts', 'path/to/modules/counter.ts'

Slide 120

Slide 120 text

4-3. tsc と CompilerAPI の概要 対象ファイル一覧から、AST(抽象構文木)が構築されます。 120 120 120 ts.Program ts.SourceFile

Slide 121

Slide 121 text

4-3. tsc と CompilerAPI の概要 その AST をもって、指定された target バージョンの JavaScript に トランスパイルされ、出力に至ります。 121 121 121 emitFiles

Slide 122

Slide 122 text

4-3. tsc と CompilerAPI の概要 この一連処理と同等の、処理を行うことができる API 群が CompilerAPI です。 122 122 122

Slide 123

Slide 123 text

4-3. tsc と CompilerAPI の概要 TypeScript パッケージに標準で備わっており、 Node.js から取り扱うこのAPI。 123 123 123

Slide 124

Slide 124 text

4-3. tsc と CompilerAPI の概要 この機能は、formatter や linter によく使われています。 コードエディタがエラーを検知する機能も、これを使っています。 124 124 124

Slide 125

Slide 125 text

4-3. tsc と CompilerAPI の概要 コードの断片をプログラム上で扱うことができるので、 code generator としても利用可能というわけです。 125 125 125

Slide 126

Slide 126 text

4-4. AST を辿る

Slide 127

Slide 127 text

4-4. AST を辿る 先程捉えた参照型を再確認してみます。 これを生成するためには、 参照文字列や関数名を知る必要がありました。 127 127 127 doneCount: ReturnType

Slide 128

Slide 128 text

4-4. AST を辿る TypeScript AST が構築されれば 「どこに・なにが・どんな名称で」 定義されているのか全てを把握することができます。 128 128 128 doneCount: ReturnType where what whatName

Slide 129

Slide 129 text

4-4. AST を辿る 特定の名称(例えばgetters)の定義を特定し、 そこに含まれる関数名一覧を取得することができます。 129 129 129 doneItems: ReturnType, visibleItems: ReturnType, doneCount: ReturnType,

Slide 130

Slide 130 text

4-4. AST を辿る 名称一覧を取得したうえで、この型定義を創り出す。 これが可能になるタイミングはどこでしょうか? 130 130 130 doneItems: ReturnType, visibleItems: ReturnType, doneCount: ReturnType,

Slide 131

Slide 131 text

4-4. AST を辿る 取得可能になるのはここ、AST が構築された直後です。 131 131 131 ts.Program ts.SourceFile

Slide 132

Slide 132 text

4-4. AST を辿る store ディレクトリに定義された全関数名は、把握された状態です。 132 132 132 ts.Program ts.SourceFile

Slide 133

Slide 133 text

4-4. AST を辿る Nuxt.js の Vuex Module Mode は、 規約に従います。 Node.js のファイルシステムが 開発をサポートします。 133 133 133 store ├── counter │ └── index.ts ├── index.ts └── todos └── index.ts

Slide 134

Slide 134 text

4-4. AST を辿る Vuex Module Mode ならではの、 このファイルツリー構造規約が、 今回のアイディアに合致しました。 134 134 134 store ├── counter │ └── index.ts ├── index.ts └── todos └── index.ts "todos/todosCount": ReturnType

Slide 135

Slide 135 text

4-4. AST を辿る プロパティキーに相当する文字列は、 ファイルパスから辿ることが できるためです。 135 135 135 "todos/todosCount": ReturnType store ├── counter │ └── index.ts ├── index.ts └── todos └── index.ts

Slide 136

Slide 136 text

4-4. AST を辿る 型推論では不可能だった、 文字列型の合成は、 Node.js にとっては容易いことです。 136 136 136 "todos/todosCount": ReturnType store ├── counter │ └── index.ts ├── index.ts └── todos └── index.ts

Slide 137

Slide 137 text

4-4. AST を辿る AST から得た関数名は、 この様に文字列合成され、 参照型の一部を担います。 137 137 137 "todos/todosCount": ReturnType store ├── counter │ └── index.ts ├── index.ts └── todos └── index.ts

Slide 138

Slide 138 text

4-4. AST を辿る 文字列型は、TypeScript の強推論に はなくてはならないものです。 それは、Lookup を可能にします。 138 138 138 "todos/todosCount": ReturnType store ├── counter │ └── index.ts ├── index.ts └── todos └── index.ts

Slide 139

Slide 139 text

4-4. AST を辿る 文字列型を自由に創出可能 ということは、大きな可能性を 意味します。 139 139 139 "todos/todosCount": ReturnType store ├── counter │ └── index.ts ├── index.ts └── todos └── index.ts

Slide 140

Slide 140 text

4-5. AST を創る

Slide 141

Slide 141 text

4-5. AST を創る CompilerAPI は、その SRC コードを パースするに留まりません。 ASTを構築するための、 ファクトリ関数群があります。 141 141 141 import 'vuex' ts.createImportDeclaration( undefined, undefined, undefined, ts.createStringLiteral('vuex') )

Slide 142

Slide 142 text

4-5. AST を創る たとえば、この様な SRC コードを生成したい場合。 142 142 142 import 'vuex' ts.createImportDeclaration( undefined, undefined, undefined, ts.createStringLiteral('vuex') )

Slide 143

Slide 143 text

4-5. AST を創る これに相当する AST は、 TypeScript から提供されている このファクトリ関数をもって 創ることができます。 143 143 143 import 'vuex' ts.createImportDeclaration( undefined, undefined, undefined, ts.createStringLiteral('vuex') )

Slide 144

Slide 144 text

4-5. AST を創る ファクトリ関数に変数を与えれば、 任意の SRC コードを 創ることができます。 144 144 144 import 'vuex' ts.createImportDeclaration( undefined, undefined, undefined, ts.createStringLiteral('vuex') )

Slide 145

Slide 145 text

4-5. AST を創る たとえば、この様な SRC コードを生成したい場合。 145 145 145 import * as Module from '/path/to/module' ts.createImportDeclaration( undefined, undefined, ts.createImportClause( undefined, ts.createNamespaceImport( ts.createIdentifier('Module') ) ), ts.createStringLiteral('/path/to/module') )

Slide 146

Slide 146 text

4-5. AST を創る これに相当する AST は、 TypeScript から提供されている このファクトリ関数をもって 創ることができます。 146 146 146 import * as Module from '/path/to/module' ts.createImportDeclaration( undefined, undefined, ts.createImportClause( undefined, ts.createNamespaceImport( ts.createIdentifier('Module') ) ), ts.createStringLiteral('/path/to/module') )

Slide 147

Slide 147 text

4-5. AST を創る ファクトリ関数に変数を与えれば、 任意の SRC コードを 創ることができます。 147 147 147 import * as Module from '/path/to/module' ts.createImportDeclaration( undefined, undefined, ts.createImportClause( undefined, ts.createNamespaceImport( ts.createIdentifier('Module') ) ), ts.createStringLiteral('/path/to/module') )

Slide 148

Slide 148 text

4-5. AST を創る AST ファクトリ関数を組み合わせ、 目的に沿った型定義を 創り出すことができます。 148 148 148 import * as Module from '/path/to/module' ts.createImportDeclaration( undefined, undefined, ts.createImportClause( undefined, ts.createNamespaceImport( ts.createIdentifier('Module') ) ), ts.createStringLiteral('/path/to/module') )

Slide 149

Slide 149 text

まとめ Summary

Slide 150

Slide 150 text

まとめ こうして、全てのピースが揃いました。 必要なのは「欠落した参照を補う」こと。 それは SourceMap File に似たものでした。 150 150 150

Slide 151

Slide 151 text

まとめ バックグラウンドで起動したこのツールは、 store ディレクトリを監視し、 変化があるたびASTを再構築、 参照型を出力します。 151 151 151

Slide 152

Slide 152 text

まとめ 開発者は複雑なことを考える必要もなく、 本来の開発に集中することが出来ます。 152 152 152

Slide 153

Slide 153 text

本日ご紹介した W型パズルは、 多くの未解決課題を解決する、 ポテンシャルを 秘めていると思います。 153 153 153 まとめ

Slide 154

Slide 154 text

ライブラリが抱える型課題は、 多角的アプローチにより、 紐解かれる日が近いかもしれません。 154 154 154 まとめ

Slide 155

Slide 155 text

vuex-guardian https://github.com/takefumi-yoshii/vuex-guardian TypeScript CompilerAPI - 創出の落書帳 - https://booth.pm/ja/items/1575217 Vuex型推論・最終章 - vuex-guardian - https://qiita.com/Takepepe/items/593d2d9e7dfd38eddbd4 155 155 155 参考文献

Slide 156

Slide 156 text

ご静聴ありがとうございました