Upgrade to Pro — share decks privately, control downloads, hide ads and more …

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

Takepepe
December 11, 2019

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

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

Takepepe

December 11, 2019
Tweet

More Decks by Takepepe

Other Decks in Technology

Transcript

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

    View full-size slide

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

    View full-size slide

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

    View full-size slide

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

    View full-size slide

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

    View full-size slide

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

    View full-size slide

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

    View full-size slide

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

    View full-size slide

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

    View full-size slide

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

    View full-size slide

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

    View full-size slide

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

    View full-size slide

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

    View full-size slide

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

    View full-size slide

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

    View full-size slide

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

    View full-size slide

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

    View full-size slide

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

    View full-size slide

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

    View full-size slide

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

    View full-size slide

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

    View full-size slide

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

    View full-size slide

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

    View full-size slide

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

    View full-size slide

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

    View full-size slide

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

    View full-size slide

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

    View full-size slide

  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

    View full-size slide

  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

    View full-size slide

  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

    View full-size slide

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

    View full-size slide

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

    View full-size slide

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

    View full-size slide

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

    View full-size slide

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

    View full-size slide

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

    View full-size slide

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

    View full-size slide

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

    View full-size slide

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

    View full-size slide

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

    View full-size slide

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

    View full-size slide

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

    View full-size slide

  43. 2-3. vuex-guardian code base

    View full-size slide

  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 })
    }
    }

    View full-size slide

  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 })
    }
    }

    View full-size slide

  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 })
    }
    }

    View full-size slide

  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 })
    }
    }

    View full-size slide

  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 })
    }
    }

    View full-size slide

  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 })
    }
    }

    View full-size slide

  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 })
    }
    }

    View full-size slide

  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 })
    }
    }

    View full-size slide

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

    View full-size slide

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

    View full-size slide

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

    View full-size slide

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

    View full-size slide

  56. 3. 導出の章
    Derivation chapter

    View full-size slide

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

    View full-size slide

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

    View full-size slide

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

    View full-size slide

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

    View full-size slide

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

    View full-size slide

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

    View full-size slide

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

    View full-size slide

  64. 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

    View full-size slide

  65. 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

    View full-size slide

  66. 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

    View full-size slide

  67. 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

    View full-size slide

  68. 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

    View full-size slide

  69. 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 }

    View full-size slide

  70. 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 }

    View full-size slide

  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 type T = { todo: Todo }

    View full-size slide

  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 type T = { todo: Todo }

    View full-size slide

  73. 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

    View full-size slide

  74. 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']

    View full-size slide

  75. 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

    View full-size slide

  76. 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

    View full-size slide

  77. 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

    View full-size slide

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

    View full-size slide

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

    View full-size slide

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

    View full-size slide

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

    View full-size slide

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

    View full-size slide

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

    View full-size slide

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

    View full-size slide

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

    View full-size slide

  86. 3-4. 宣言空間の活用

    View full-size slide

  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']
    }
    }

    View full-size slide

  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']
    }
    }

    View full-size slide

  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']
    }
    }

    View full-size slide

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

    View full-size slide

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

    View full-size slide

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

    View full-size slide

  93. 3-5. 文字列参照と Lookup

    View full-size slide

  94. 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 })

    View full-size slide

  95. 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 })

    View full-size slide

  96. 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 })

    View full-size slide

  97. 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 })

    View full-size slide

  98. 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 })

    View full-size slide

  99. 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 })

    View full-size slide

  100. 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 })

    View full-size slide

  101. 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 })

    View full-size slide

  102. 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 })

    View full-size slide

  103. 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 })

    View full-size slide

  104. 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 })

    View full-size slide

  105. 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 })

    View full-size slide

  106. 4. 創出の章
    Creation chapter

    View full-size slide

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

    View full-size slide

  108. 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']
    }

    View full-size slide

  109. 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']
    }

    View full-size slide

  110. 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']
    }

    View full-size slide

  111. 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']
    }

    View full-size slide

  112. 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']
    }

    View full-size slide

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

    View full-size slide

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

    View full-size slide

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

    View full-size slide

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

    View full-size slide

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

    View full-size slide

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

    View full-size slide

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

    View full-size slide

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

    View full-size slide

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

    View full-size slide

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

    View full-size slide

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

    View full-size slide

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

    View full-size slide

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

    View full-size slide

  126. 4-4. AST を辿る

    View full-size slide

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

    View full-size slide

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

    View full-size slide

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

    View full-size slide

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

    View full-size slide

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

    View full-size slide

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

    View full-size slide

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

    View full-size slide

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

    View full-size slide

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

    View full-size slide

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

    View full-size slide

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

    View full-size slide

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

    View full-size slide

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

    View full-size slide

  140. 4-5. AST を創る

    View full-size slide

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

    View full-size slide

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

    View full-size slide

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

    View full-size slide

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

    View full-size slide

  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')
    )

    View full-size slide

  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')
    )

    View full-size slide

  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')
    )

    View full-size slide

  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')
    )

    View full-size slide

  149. まとめ
    Summary

    View full-size slide

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

    View full-size slide

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

    View full-size slide

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

    View full-size slide

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

    View full-size slide

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

    View full-size slide

  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
    参考文献

    View full-size slide

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

    View full-size slide