TypeScript CompilerAPI によるVuexの参照型生成

5d9cd19df0e91caac118b793b4f803d5?s=47 Takepepe
December 11, 2019

TypeScript CompilerAPI によるVuexの参照型生成

【PLAID × ラクスル】Vue.js for 2020

5d9cd19df0e91caac118b793b4f803d5?s=128

Takepepe

December 11, 2019
Tweet

Transcript

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

  2. About Me ▪ Takefumi Yoshii / @Takepepe ▪ DeNA /

    DeSC Healthcare ▪ Frontend Engineer ▪ TypeScript Meetup JP member 2
  3. Agenda ▪ 1. Vuex型推論への挑戦 ▪ 2. 辿りついた一つの解法 ▪ 3. 導出の章

    ▪ 4. 創出の章 3
  4. 1. Vuex型推論への挑戦 Challenge to Vuex type type inference

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

  28. 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
  29. 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
  30. 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
  31. 1-4. 不自然なコードベース 多くの型定義を必要としないコードこそ TypeScript らしいコードであり、 親しまれている「DX」だと私は考えています。 その「DX」は Vue.js や Vuex

    らしい、 守りたいアイデンティティです。 31 31 31
  32. 2. 辿りついた一つの解法 One solution I arrived at

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

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

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

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

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

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

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

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

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

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

  43. 2-3. vuex-guardian code base

  44. 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 }) } }
  45. 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 }) } }
  46. 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 }) } }
  47. 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 }) } }
  48. 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 }) } }
  49. 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 }) } }
  50. 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 }) } }
  51. 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 }) } }
  52. 2-3. vuex-guardian code base 自然なコードベースのため、 TypeScript 初学者であっても、 実装に悩むことはほとんどないでしょう。 52 52

    52
  53. 2-3. vuex-guardian code base 少し調整が必要ですが、 SFC における Vue.extend 記法であっても、 型推論は行き届きます。

    53 53 53 参考:https://qiita.com/Takepepe/items/593d2d9e7dfd38eddbd4
  54. 2-3. vuex-guardian code base 必要なのは、このツールを バックグラウンドで起動しておくことのみです。 54 54 54 $

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

  56. 3. 導出の章 Derivation chapter

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

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

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

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

  61. 3-2. 型の部分抽出

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

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

    todo: Todo) { state.todos.push(todo) }
  64. 3-2. 型の部分抽出 次の「A2」の様な補助型を利用すると、第二引数型が抽出できます。 64 64 64 type A2<T> = T

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

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

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

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

    extends (a1: any, a2: infer I) => any ? I : never type T = typeof addTodo type U = A2<typeof addTodo> type T = (state: State, todo: Todo) => void type U = Todo inferred
  69. 3-2. 型の部分抽出 実際に mutation 関数から型を抽出してみます。 69 69 69 export const

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

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

    = { addTodo(state: State, payload: { todo: Todo }) { state.todos.push(payload.todo) } } type T = A2<typeof mutations['addTodo']> type T = { todo: Todo }
  72. 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<typeof mutations['addTodo']> type T = { todo: Todo }
  73. 3-2. 型の部分抽出 次に、getter 関数を見ていきましょう。 73 73 73 export const getters

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

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

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

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

    { doneCount(state: State) { return state.todos.filter(todo => todo.done).length } } type T = ReturnType<typeof getters['doneCount']> type T = number
  78. 3-2. 型の部分抽出 ここまでの様に、ひとつひとつの 「定義済み実装」から型を抽出することで、 信頼できる「型の一次ソース」として 正しく型を参照することができます。 78 78 78

  79. 3-3. 型の文字列参照

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

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

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

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

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

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

    LocalGetters['todos']['doneItems'] 'todos/visibleItems': LocalGetters['todos']['visibleItems'] 'todos/doneCount': LocalGetters['todos']['doneCount'] }
  86. 3-4. 宣言空間の活用

  87. 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'] } }
  88. 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'] } }
  89. 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'] } }
  90. 3-4. 宣言空間の活用 vuex-guardian は、 より強固に定義された 「StrictStore型」を 持ちます。 90 90 90

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

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

    92 92 import 'vuex' declare module 'vuex' { interface StrictStore extends Store<RootState> { getters: RootGetters commit: StrictCommit<MutationTypes> dispatch: StrictDispatch<ActionTypes> } }
  93. 3-5. 文字列参照と Lookup

  94. 3-5. 文字列参照と Lookup $store.dispatch について確認していきましょう。 94 94 94 interface ActionTypes

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

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

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

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

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

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

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

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

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

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

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

    { 'todos/acyncAddTodo': LocalActionTypes['todos']['acyncAddTodo'] 'todos/acyncDoneTodo': LocalActionTypes['todos']['acyncDoneTodo'] } interface StrictDispatch<A> { <T extends keyof A>(type: T, payload?: A[T], options?: DispatchOptions): Promise<void> } this.$store.dispatch('todos/acyncDoneTodo', { id: this.data.id })
  106. 4. 創出の章 Creation chapter

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

  108. 4-1. 創出の型パズル「Compiler API」 こうして全ての参照型を集約し 強化された「StrictStore型」は、 SFC の $store に適用されます。 108

    108 108 interface LocalGetters { todos: { todosCount:ReturnType<Modules['todos']['getters']['todosCount']> doneCount: ReturnType<Modules['todos']['getters']['doneCount']> } } 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'] }
  109. 4-1. 創出の型パズル「Compiler API」 そのため SFC であっても、 Vuex における変化を、 コンパイルエラーとして 検出することが出来ます。

    109 109 109 interface LocalGetters { todos: { todosCount:ReturnType<Modules['todos']['getters']['todosCount']> doneCount: ReturnType<Modules['todos']['getters']['doneCount']> } } 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'] }
  110. 4-1. 創出の型パズル「Compiler API」 しかし、参照型の量は膨大です。 手動でこの型を定義していくのは、 あまりにも骨が折れます。 110 110 110 interface

    LocalGetters { todos: { todosCount:ReturnType<Modules['todos']['getters']['todosCount']> doneCount: ReturnType<Modules['todos']['getters']['doneCount']> } } 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'] }
  111. 4-1. 創出の型パズル「Compiler API」 リファクタリングしようものなら、 多大な修正を強いられます。 これは求めたものではありません。 111 111 111 interface

    LocalGetters { todos: { todosCount:ReturnType<Modules['todos']['getters']['todosCount']> doneCount: ReturnType<Modules['todos']['getters']['doneCount']> } } 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'] }
  112. 4-1. 創出の型パズル「Compiler API」 この「参照型」を自動生成 するために最適な機能が 「CompilerAPI」です。 112 112 112 interface

    LocalGetters { todos: { todosCount:ReturnType<Modules['todos']['getters']['todosCount']> doneCount: ReturnType<Modules['todos']['getters']['doneCount']> } } 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'] }
  113. 4-2. DEMO それでは実際に、参照型が出力される デモ動画を用意しましたので、 ご覧ください。 113 113 113

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

  115. 4-3. tsc と CompilerAPI の概要

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

    116 116 116
  117. 4-3. tsc と CompilerAPI の概要 tsc は TypeScript プロジェクトを JavaScript

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

    118 118 tsconfig
  119. 4-3. tsc と CompilerAPI の概要 そこに記された設定から、対象 SRC となるファイルパス一覧を取得。 119 119

    119 'path/to/modules/todo.ts', 'path/to/modules/counter.ts'
  120. 4-3. tsc と CompilerAPI の概要 対象ファイル一覧から、AST(抽象構文木)が構築されます。 120 120 120 ts.Program

    ts.SourceFile
  121. 4-3. tsc と CompilerAPI の概要 その AST をもって、指定された target バージョンの

    JavaScript に トランスパイルされ、出力に至ります。 121 121 121 emitFiles
  122. 4-3. tsc と CompilerAPI の概要 この一連処理と同等の、処理を行うことができる API 群が CompilerAPI です。

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

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

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

    125 125
  126. 4-4. AST を辿る

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

    ReturnType<Modules['todos']['getters']['doneCount']>
  128. 4-4. AST を辿る TypeScript AST が構築されれば 「どこに・なにが・どんな名称で」 定義されているのか全てを把握することができます。 128 128

    128 doneCount: ReturnType<Modules['todos']['getters']['doneCount']> where what whatName
  129. 4-4. AST を辿る 特定の名称(例えばgetters)の定義を特定し、 そこに含まれる関数名一覧を取得することができます。 129 129 129 doneItems: ReturnType<Modules['todos']['getters']['doneItems']>,

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

    visibleItems: ReturnType<Modules['todos']['getters']['visibleItems']>, doneCount: ReturnType<Modules['todos']['getters']['doneCount']>,
  131. 4-4. AST を辿る 取得可能になるのはここ、AST が構築された直後です。 131 131 131 ts.Program ts.SourceFile

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

  133. 4-4. AST を辿る Nuxt.js の Vuex Module Mode は、 規約に従います。

    Node.js のファイルシステムが 開発をサポートします。 133 133 133 store ├── counter │ └── index.ts ├── index.ts └── todos └── index.ts
  134. 4-4. AST を辿る Vuex Module Mode ならではの、 このファイルツリー構造規約が、 今回のアイディアに合致しました。 134

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

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

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

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

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

    ReturnType<Modules["todos"]["getters"]["todosCount"]> store ├── counter │ └── index.ts ├── index.ts └── todos └── index.ts
  140. 4-5. AST を創る

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

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

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

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

    144 import 'vuex' ts.createImportDeclaration( undefined, undefined, undefined, ts.createStringLiteral('vuex') )
  145. 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') )
  146. 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') )
  147. 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') )
  148. 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') )
  149. まとめ Summary

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

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

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

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

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

  155. 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 参考文献
  156. ご静聴ありがとうございました