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 Slide

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

    View Slide

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

    View Slide

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

    View Slide

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

    View Slide

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

    View Slide

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

    View Slide

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

    View Slide

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

    View Slide

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

    View Slide

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

    View Slide

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

    View Slide

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

    View Slide

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

    View Slide

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

    View Slide

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

    View Slide

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

    View Slide

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

    View Slide

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

    View Slide

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

    View Slide

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

    View Slide

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

    View Slide

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

    View Slide

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

    View Slide

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

    View Slide

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

    View Slide

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

    View 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 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 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 Slide

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

    View Slide

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

    View Slide

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

    View Slide

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

    View Slide

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

    View Slide

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

    View Slide

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

    View Slide

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

    View Slide

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

    View Slide

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

    View Slide

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

    View Slide

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

    View Slide

  43. 2-3. vuex-guardian code base

    View 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 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 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 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 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 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 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 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 Slide

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

    View Slide

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

    View Slide

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

    View Slide

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

    View Slide

  56. 3. 導出の章
    Derivation chapter

    View Slide

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

    View Slide

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

    View Slide

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

    View Slide

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

    View Slide

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

    View Slide

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

    View Slide

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

    View 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 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 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 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 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 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 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 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 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 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 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 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 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 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 Slide

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

    View Slide

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

    View Slide

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

    View Slide

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

    View Slide

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

    View Slide

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

    View 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 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 Slide

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

    View 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 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 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 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 Slide

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

    View 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 Slide

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

    View 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 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 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 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 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 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 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 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 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 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 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 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 Slide

  106. 4. 創出の章
    Creation chapter

    View Slide

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

    View 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 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 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 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 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 Slide

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

    View Slide

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

    View Slide

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

    View Slide

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

    View Slide

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

    View Slide

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

    View Slide

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

    View Slide

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

    View Slide

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

    View Slide

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

    View Slide

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

    View Slide

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

    View Slide

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

    View Slide

  126. 4-4. AST を辿る

    View Slide

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

    View Slide

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

    View Slide

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

    View Slide

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

    View Slide

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

    View Slide

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

    View 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 Slide

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

    View Slide

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

    View Slide

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

    View Slide

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

    View Slide

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

    View Slide

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

    View Slide

  140. 4-5. AST を創る

    View Slide

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

    View Slide

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

    View Slide

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

    View Slide

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

    View 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 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 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 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 Slide

  149. まとめ
    Summary

    View Slide

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

    View Slide

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

    View Slide

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

    View Slide

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

    View Slide

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

    View 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 Slide

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

    View Slide