Meta Library VS Meta Type Definitions

5d9cd19df0e91caac118b793b4f803d5?s=47 Takepepe
July 10, 2019

Meta Library VS Meta Type Definitions

5d9cd19df0e91caac118b793b4f803d5?s=128

Takepepe

July 10, 2019
Tweet

Transcript

  1. Meta Library VS Meta Type Definitions TypeScript Meetup #2 @Takepepe

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

    DeSC Healthcare ▪ Frontend Engineer ▪ 「型の強化書・実践 TypeScript」著者
  3. 実践 TypeScript 発売から一週間で増刷確定しました! Amazon「ITコンピュータ・IT 関連」で 干し芋のランキング1位獲得 (一時的)

  4. Agenda 「実践 TypeScript」に残した課題を、 「TypeScript Compiler API」で解決します。

  5. Agenda 掲題の「Meta Library」は Vue.js でお馴染みの「Vuex」のこと。 ▪ 5分でわかる Vuex の概要と型課題 ▪

    推論観点で紐解く Vuex に必要な型定義 ▪ TypeScript Compiler API で壁を超える
  6. 5分でわかる Vuex の概要と型課題

  7. Vuex - State management - ▪ 状態管理ライブラリの Vuex。Nuxt.js は標準で入る。 store

    ├── counter │ └── index.ts └── todos └── index.ts
  8. Vuex - State management - ▪ 関心の境界で Module を定義 store

    ├── counter │ └── index.ts └── todos └── index.ts
  9. Vuex - State management - ▪ Module は state を保持

    store ├── counter │ └── index.ts └── todos └── index.ts store ├── counter │ └── { count: number } └── todos └── { todos: Todo[] }
  10. Vuex - State management - ▪ todos の実装を確認していきます store ├──

    counter │ └── index.ts └── todos └── index.ts store ├── counter │ └── { count: number } └── todos └── { todos: Todo[] }
  11. Vuex - Inside Module - ▪ Module が保持する state ファクトリ関数

    export const state = () => ({ todos: [] }) EX : store/todos/index.ts
  12. Vuex - Inside Module - ▪ 算出プロパティを実装する getters export const

    state = () => ({ todos: [] }) export const getters = { todosCount(state) { return state.todos.length }, doneCount(state) { return state.todos.filter(todo => todo.done).length } } EX : store/todos/index.ts
  13. Vuex - Inside Module - ▪ 自動で引数に挿さる参照がたくさん export const state

    = () => ({ todos: [] }) export const getters = { todosCount(state, getters, rootState, rootGetters) { const counterCount = rootState.counter.count const counterDouble = rootGetters["counter/double"] return state.todos.length }, doneCount(state, getters, rootState, rootGetters) { return state.todos.filter(todo => todo.done).length } } EX : store/todos/index.ts
  14. Vuex - Inside Module - ▪ 自動で挿さる参照は4つ export const state

    = () => ({ todos: [] }) export const getters = { todosCount(state, getters, rootState, rootGetters) { const counterCount = rootState.counter.count const counterDouble = rootGetters["counter/double"] return state.todos.length }, doneCount(state, getters, rootState, rootGetters) { return state.todos.filter(todo => todo.done).length } } 第一引数:state インスタンス参照(local) 第二引数:getter 関数同士の参照(local) 第三引数:store state の root 参照 第四引数:getter 関数 の root 参照 EX : store/todos/index.ts
  15. Vuex - Inside Module - ▪ ファクトリ関数で生成された state インスタンス export

    const state = () => ({ todos: [] }) export const getters = { todosCount(state, getters, rootState, rootGetters) { const counterCount = rootState.counter.count const counterDouble = rootGetters["counter/double"] return state.todos.length }, doneCount(state, getters, rootState, rootGetters) { return state.todos.filter(todo => todo.done).length } } 第一引数:state インスタンス参照(local) 第二引数:getter 関数同士の参照(local) 第三引数:store state の root 参照 第四引数:getter 関数 の root 参照 EX : store/todos/index.ts
  16. Vuex - Inside Module - ▪ Module 内部の getter 関数参照

    export const state = () => ({ todos: [] }) export const getters = { todosCount(state, getters, rootState, rootGetters) { const counterCount = rootState.counter.count const counterDouble = rootGetters["counter/double"] return state.todos.length }, doneCount(state, getters, rootState, rootGetters) { return state.todos.filter(todo => todo.done).length } } 第一引数:state インスタンス参照(local) 第二引数:getter 関数同士の参照(local) 第三引数:store state の root 参照 第四引数:getter 関数 の root 参照 EX : store/todos/index.ts
  17. Vuex - Inside Module - ▪ rootState は他 Module への参照を持つ

    export const state = () => ({ todos: [] }) export const getters = { todosCount(state, getters, rootState, rootGetters) { const counterCount = rootState.counter.count const counterDouble = rootGetters["counter/double"] return state.todos.length }, doneCount(state, getters, rootState, rootGetters) { return state.todos.filter(todo => todo.done).length } } 第一引数:state インスタンス参照(local) 第二引数:getter 関数同士の参照(local) 第三引数:store state の root 参照 第四引数:getter 関数 の root 参照 EX : store/todos/index.ts
  18. Vuex - Inside Module - ▪ rootGetters は他 Module への参照を持つ

    export const state = () => ({ todos: [] }) export const getters = { todosCount(state, getters, rootState, rootGetters) { const counterCount = rootState.counter.count const counterDouble = rootGetters["counter/double"] return state.todos.length }, doneCount(state, getters, rootState, rootGetters) { return state.todos.filter(todo => todo.done).length } } 第一引数:state インスタンス参照(local) 第二引数:getter 関数同士の参照(local) 第三引数:store state の root 参照 第四引数:getter 関数 の root 参照 EX : store/todos/index.ts
  19. Vuex - Literal Reference - ▪ getter 関数の文字列参照 store ├──

    counter │ └── index.ts └── todos └── index.ts store ├── counter │ ├── function double(): number │ └── function expo(): number └── todos ├── function todosCount(): number └── function doneCount(): number
  20. Vuex - Literal Reference - ▪ getter 関数の文字列参照(ツリー構造に起因) store ├──

    counter │ └── index.ts └── todos └── index.ts store ├── counter │ ├── "counter/double" │ └── "counter/expo" └── todos ├── "todos/todosCount" └── "todos/doneCount"
  21. Vuex - Literal Reference - ▪ getter 関数の文字列参照(ツリー構造に起因) store ├──

    counter │ └── index.ts └── todos └── index.ts store ├── counter │ ├── "counter/double": number │ └── "counter/expo": number └── todos ├── "todos/todosCount": number └── "todos/doneCount": number getter 関数は、関数ではなく値として見える
  22. Vuex - Literal Reference - ▪ getter 関数の文字列参照(SFC [Component] から)

    get double() { return this.$store.getters['counter/double'] } get expo2() { return this.$store.getters['counter/expo2'] } EX : components/example.vue
  23. Vuex - Literal Reference - ▪ getter 関数の文字列参照(型推論が any …orz)

    get double() { return this.$store.getters['counter/double'] // any } get expo2() { return this.$store.getters['counter/expo2'] // any } EX : components/example.vue
  24. Vuex - Literal Reference - ▪ store.commit 関数の文字列参照 store ├──

    counter │ └── index.ts └── todos └── index.ts store ├── counter │ ├── function setCount(n: number): void │ ├── function decrement(): void │ └── function increment(): void └── todos ├── function addTodo(todo: Todo): void └── function doneTodo(id: string): void 「mutation」という状態を変更する関数群
  25. Vuex - Literal Reference - ▪ store.commit 関数の文字列参照(ツリー構造に起因) store ├──

    counter │ └── index.ts └── todos └── index.ts store ├── counter │ ├── "counter/setCount" │ ├── "counter/decrement" │ └── "counter/increment" └── todos ├── "todos/addTodo" └── "todos/doneTodo" 生成された一意な文字列は「mutation type」と呼ばれる
  26. Vuex - Literal Reference - ▪ store.commit 関数の文字列参照(SFC [Component] から)

    addTodo() { this.$store.commit('todos/addTodo',{ todo: { id: uuid(), createdAt: new Date(), task: this.todoTask, done: false } }) } doneTodo() { this.$store.commit('todos/doneTodo',{ id: this.id }) } EX : components/example.vue
  27. Vuex - Literal Reference - ▪ store.commit 関数の文字列参照(型推論が any …orz)

    addTodo() { this.$store.commit('todos/addTodo',{ todo: { id: uuid(), createdAt: new Date(), task: this.todoTask, done: false } }) } doneTodo() { this.$store.commit('todos/doneTodo',{ id: this.id }) } EX : components/example.vue
  28. Vuex - Type Issues - ▪ 勘所が any に落ちている ▪

    文字列参照と payload の「組」が担保できていない ▪ Generics による補正も効かない ▪ Module 名を変更しようものなら、影響範囲が甚大
  29. 公式型定義では無理がある。 これはまずい、何とかしたい。

  30. TypeScript の「型推論」観点 + 「メタ型定義」で挑む

  31. 推論観点で紐解く Vuex に必要な型定義

  32. Getters ❌ state, ❌ getters, ❌ rootState, ❌ rootGetters 第一引数

    State 型「 type S 」 export const getters = { todosCount(state, getters, rootState, rootGetters) { ... }, doneCount(state, getters, rootState, rootGetters) { ... } } export type S = { todos: Todo[] }
  33. Getters ❌ state, ❌ getters, ❌ rootState, ❌ rootGetters getters

    向け「 type Getters 」 export const getters: Getters<S> = { todosCount(state, getters, rootState, rootGetters) { ... }, doneCount(state, getters, rootState, rootGetters) { ... } } type Getters<S> = { } export type S = { todos: Todo[] }
  34. Getters ✅ state, ❌ getters, ❌ rootState, ❌ rootGetters 第一引数を「Index

    Signature」で一律付与 export const getters: Getters<S> = { todosCount(state, getters, rootState, rootGetters) { ... }, doneCount(state, getters, rootState, rootGetters) { ... } } type Getters<S> = { [k: string]: (state: S) => unknown } export type S = { todos: Todo[] }
  35. Getters ✅ state, ❌ getters, ❌ rootState, ❌ rootGetters getter

    関数のメタ型定義「 type G 」 export const getters: Getters<S, G> = { todosCount(state, getters, rootState, rootGetters) { ... }, doneCount(state, getters, rootState, rootGetters) { ... } } type Getters<S, G> = { [k: string]: (state: S, getters: G) => unknown } export type S = { todos: Todo[] } export type G = { }
  36. Getters ✅ state, ❌ getters, ❌ rootState, ❌ rootGetters 外部参照に向けた「期待型」を定義

    export const getters: Getters<S, G> = { todosCount(state, getters, rootState, rootGetters) { ... }, doneCount(state, getters, rootState, rootGetters) { ... } } type Getters<S, G> = { [k: string]: (state: S, getters: G) => unknown } export type S = { todos: Todo[] } export type G = { todosCount: number doneCount: number }
  37. Getters ✅ state, ❌ getters, ❌ rootState, ❌ rootGetters 外部参照に向けた「期待型」を定義

    export const getters: Getters<S, G> = { todosCount(state, getters, rootState, rootGetters) { ... }, doneCount(state, getters, rootState, rootGetters) { ... } } type Getters<S, G> = { [k: string]: (state: S, getters: G) => unknown } export type S = { todos: Todo[] } export type G = { todosCount: number doneCount: number } getter 関数は外からは値として見える
  38. Getters ✅ state, ✅ getters, ❌ rootState, ❌ rootGetters 戻り型を「Mapped

    Types」で特定( G[K] ) export const getters: Getters<S, G> = { todosCount(state, getters, rootState, rootGetters) { ... }, doneCount(state, getters, rootState, rootGetters) { ... } } type Getters<S, G> = { [K in keyof G]: (state: S, getters: G) => G[K] } export type S = { todos: Todo[] } export type G = { todosCount: number doneCount: number } getter 関数は期待型を返す実装をしなければならない
  39. Getters ✅ state, ✅ getters, ❌ rootState, ❌ rootGetters ツリー構造に則した「

    type RootState 」 export const getters: Getters<S, G> = { todosCount(state, getters, rootState, rootGetters) { ... }, doneCount(state, getters, rootState, rootGetters) { ... } } type Getters<S, G> = { [K in keyof G]: ( state: S, getters: G, rootState: RootState ) => G[K] } export type S = { todos: Todo[] } export type G = { todosCount: number doneCount: number } type RootState = { }
  40. Getters ✅ state, ✅ getters, ✅ rootState, ❌ rootGetters 型を

    import して組み立てる export const getters: Getters<S, G> = { todosCount(state, getters, rootState, rootGetters) { ... }, doneCount(state, getters, rootState, rootGetters) { ... } } type Getters<S, G> = { [K in keyof G]: ( state: S, getters: G, rootState: RootState ) => G[K] } export type S = { todos: Todo[] } export type G = { todosCount: number doneCount: number } type RootState = { todos: Todos.S counter: Counter.S }
  41. Getters ✅ state, ✅ getters, ✅ rootState, ❌ rootGetters 文字列参照のメタ型定義「

    type RG 」 export const getters: Getters<S, G> = { todosCount(state, getters, rootState, rootGetters) { ... }, doneCount(state, getters, rootState, rootGetters) { ... } } type Getters<S, G> = { [K in keyof G]: ( state: S, getters: G, rootState: RootState ) => G[K] } export type S = { todos: Todo[] } export type G = { todosCount: number doneCount: number } export type RG = { } type RootState = { todos: Todos.S counter: Counter.S }
  42. Getters ✅ state, ✅ getters, ✅ rootState, ❌ rootGetters 文字列参照に「Indexed

    Access Types」でマッピング export const getters: Getters<S, G> = { todosCount(state, getters, rootState, rootGetters) { ... }, doneCount(state, getters, rootState, rootGetters) { ... } } type Getters<S, G> = { [K in keyof G]: ( state: S, getters: G, rootState: RootState ) => G[K] } export type S = { todos: Todo[] } export type G = { todosCount: number doneCount: number } export type RG = { 'todos/todosCount': G['todosCount'] 'todos/doneCount': G['doneCount'] } type RootState = { todos: Todos.S counter: Counter.S }
  43. Getters ✅ state, ✅ getters, ✅ rootState, ❌ rootGetters 文字列参照を集約した「

    type RootGetters 」 export const getters: Getters<S, G> = { todosCount(state, getters, rootState, rootGetters) { ... }, doneCount(state, getters, rootState, rootGetters) { ... } } type Getters<S, G> = { [K in keyof G]: ( state: S, getters: G, rootState: RootState, rootGetters: RootGetters ) => G[K] } export type S = { todos: Todo[] } export type G = { todosCount: number doneCount: number } export type RG = { 'todos/todosCount': G['todosCount'] 'todos/doneCount': G['doneCount'] } type RootState = { todos: Todos.S counter: Counter.S } type RootGetters = {}
  44. Getters ✅ state, ✅ getters, ✅ rootState, ✅ rootGetters 型を

    import して組み立てる export const getters: Getters<S, G> = { todosCount(state, getters, rootState, rootGetters) { ... }, doneCount(state, getters, rootState, rootGetters) { ... } } type Getters<S, G> = { [K in keyof G]: ( state: S, getters: G, rootState: RootState, rootGetters: RootGetters ) => G[K] } export type S = { todos: Todo[] } export type G = { todosCount: number doneCount: number } export type RG = { 'todos/todosCount': G['todosCount'] 'todos/doneCount': G['doneCount'] } type RootState = { todos: Todos.S counter: Counter.S } type RootGetters = Todos.RG & Counter.RG
  45. Getters ✅ state, ✅ getters, ✅ rootState, ✅ rootGetters All

    Green ! だがしかし… export const getters: Getters<S, G> = { todosCount(state, getters, rootState, rootGetters) { ... }, doneCount(state, getters, rootState, rootGetters) { ... } } type Getters<S, G> = { [K in keyof G]: ( state: S, getters: G, rootState: RootState, rootGetters: RootGetters ) => G[K] } export type S = { todos: Todo[] } export type G = { todosCount: number doneCount: number } export type RG = { 'todos/todosCount': G['todosCount'] 'todos/doneCount': G['doneCount'] } type RootState = { todos: Todos.S counter: Counter.S } type RootGetters = Todos.RG & Counter.RG
  46. Getters ✅ state, ✅ getters, ✅ rootState, ✅ rootGetters 定常開発で「メタ型定義」がこれだけ必要…

    export const getters: Getters<S, G> = { todosCount(state, getters, rootState, rootGetters) { ... }, doneCount(state, getters, rootState, rootGetters) { ... } } type Getters<S, G> = { [K in keyof G]: ( state: S, getters: G, rootState: RootState, rootGetters: RootGetters ) => G[K] } export type S = { todos: Todo[] } export type G = { todosCount: number doneCount: number } export type RG = { 'todos/todosCount': G['todosCount'] 'todos/doneCount': G['doneCount'] } type RootState = { todos: Todos.S counter: Counter.S } type RootGetters = Todos.RG & Counter.RG
  47. Getters ✅ state, ✅ getters, ✅ rootState, ✅ rootGetters ヒューマンエラーが防げず、スマートではない…

    export const getters: Getters<S, G> = { todosCount(state, getters, rootState, rootGetters) { ... }, doneCount(state, getters, rootState, rootGetters) { ... } } type Getters<S, G> = { [K in keyof G]: ( state: S, getters: G, rootState: RootState, rootGetters: RootGetters ) => G[K] } export type S = { todos: Todo[] } export type G = { todosCount: number doneCount: number } export type RG = { 'todos/todosCount': G['todosCount'] 'todos/doneCount': G['doneCount'] } type RootState = { todos: Todos.S counter: Counter.S } type RootGetters = Todos.RG & Counter.RG
  48. Getters ✅ state, ✅ getters, ✅ rootState, ✅ rootGetters ツリー構造に則した型推論は不可能では…

    export const getters: Getters<S, G> = { todosCount(state, getters, rootState, rootGetters) { ... }, doneCount(state, getters, rootState, rootGetters) { ... } } type Getters<S, G> = { [K in keyof G]: ( state: S, getters: G, rootState: RootState, rootGetters: RootGetters ) => G[K] } export type S = { todos: Todo[] } export type G = { todosCount: number doneCount: number } export type RG = { 'todos/todosCount': G['todosCount'] 'todos/doneCount': G['doneCount'] } type RootState = { todos: Todos.S counter: Counter.S } type RootGetters = Todos.RG & Counter.RG
  49. 「実践TypeScript」にはここまで書いた。 これが残してきた課題。

  50. 引用 : 実践 TypeScript(p.305)

  51. いやできる

  52. TypeScript Compiler API で壁を超える

  53. DEMO https://vimeo.com/346283058 https://github.com/takefumi-yoshii/vuex-definitions-mapper

  54. Inside Codegen 1. Watch Store Dir 2. ts.Program 3. ts.SourceFile

    4. ts.PropertySignature 5. ts.TypeAliasDeclaration 6. ts.NodeArray 7. ts.Printer 8. fs.writeFileSync
  55. 1. Watch Store Dir 型構築に必要な情報を収集 store ├── counter │ ├──

    index.ts │ └── type.ts ├── index.ts ├── todos │ ├── index.ts │ └── type.ts └── type.ts
  56. 1. Watch Store Dir DEMO では「type.ts」の変更を検知 store ├── counter │

    ├── index.ts │ └── type.ts ├── index.ts ├── todos │ ├── index.ts │ └── type.ts └── type.ts [ { fileName: 'type.ts', filePath: '~/store/counter/type.ts', namespace: 'counter', moduleName: 'COUNTER' }, { fileName: 'type.ts', filePath: '~/store/todos/type.ts', namespace: 'todos', moduleName: 'TODOS' } ]
  57. 1. Watch Store Dir 普通の Node.js コード // 定義対象ディレクトリから、定義対象ファイル一覧を取得 const

    [typeFiles, fileTree] = await getTypeDefinitions(storeDir, 'type.ts')
  58. 2. ts.Program 「ts.Program」を作成、AST を取得・Compiler API を扱う // 定義対象ディレクトリから、定義対象ファイル一覧を取得 const [typeFiles,

    fileTree] = await getTypeDefinitions(storeDir, 'type.ts') // program 用に配列を作成 const files = typeFiles.map(file => file.filePath) // AST が格納された program を作成。(files: string[]) const program = ts.createProgram(files, {}) ts.createProgram 第一引数は src ファイルパスの文字列配列
  59. 3. ts.SourceFile 「ts.SourceFile」を ts.Program から取得 function typedefs(program: ts.Program, typeFile: TypeFile,

    typeKind: string) { const sourceFile = program.getSourceFile(typeFile.filePath) if (!sourceFile) return const typeDefinitions = sourceFile.getChildAt(0) return flatten( typeDefinitions .getChildren() .filter( (node): node is ts.TypeAliasDeclaration | ts.InterfaceDeclaration => ts.isTypeAliasDeclaration(node) || ts.isInterfaceDeclaration(node) ) .filter(node => node.name.text === typeKind) .map(node => mapMembersTypeElements(node, typeFile, typeKind)) ) }
  60. 3. ts.SourceFile 型定義に限定した「ts.Node」に絞り込む function typedefs(program: ts.Program, typeFile: TypeFile, typeKind: string)

    { const sourceFile = program.getSourceFile(typeFile.filePath) if (!sourceFile) return const typeDefinitions = sourceFile.getChildAt(0) return flatten( typeDefinitions .getChildren() .filter( (node): node is ts.TypeAliasDeclaration | ts.InterfaceDeclaration => ts.isTypeAliasDeclaration(node) || ts.isInterfaceDeclaration(node) ) .filter(node => node.name.text === typeKind) .map(node => mapMembersTypeElements(node, typeFile, typeKind)) ) } ユーザー定義 type guard で絞り込む
  61. 3. ts.SourceFile 規定名称(ex: type G or interface G )に一致した型宣言に絞り込む function

    typedefs(program: ts.Program, typeFile: TypeFile, typeKind: string) { const sourceFile = program.getSourceFile(typeFile.filePath) if (!sourceFile) return const typeDefinitions = sourceFile.getChildAt(0) return flatten( typeDefinitions .getChildren() .filter( (node): node is ts.TypeAliasDeclaration | ts.InterfaceDeclaration => ts.isTypeAliasDeclaration(node) || ts.isInterfaceDeclaration(node) ) .filter(node => node.name.text === typeKind) .map(node => mapMembersTypeElements(node, typeFile, typeKind)) ) } node.name.text は src の型宣言名称
  62. 3. ts.SourceFile 変換関数で処理、型プロパティシグネチャ配列をつくる function typedefs(program: ts.Program, typeFile: TypeFile, typeKind: string)

    { const sourceFile = program.getSourceFile(typeFile.filePath) if (!sourceFile) return const typeDefinitions = sourceFile.getChildAt(0) return flatten( typeDefinitions .getChildren() .filter( (node): node is ts.TypeAliasDeclaration | ts.InterfaceDeclaration => ts.isTypeAliasDeclaration(node) || ts.isInterfaceDeclaration(node) ) .filter(node => node.name.text === typeKind) .map(node => mapMembersTypeElements(node, typeFile, typeKind)) ) }
  63. 生成したい型定義 type RootGetters = { "counter/double": COUNTER.G["double"]; "counter/expo": COUNTER.G["expo"]; "todos/todosCount":

    TODOS.G["todosCount"]; "todos/doneCount": TODOS.G["doneCount"]; };
  64. ts.PropertySignature type RootGetters = { "counter/double": COUNTER.G["double"]; "counter/expo": COUNTER.G["expo"]; "todos/todosCount":

    TODOS.G["todosCount"]; "todos/doneCount": TODOS.G["doneCount"]; }; 4. ts.PropertySignature
  65. 4. ts.PropertySignature ts.createStringLiteral ts.createPropertySignature( undefined, ts.createStringLiteral(`${typeFile.namespace}/${identifier}`), undefined, ts.createIndexedAccessTypeNode( ts.createTypeReferenceNode( ts.createQualifiedName(

    ts.createIdentifier(typeFile.moduleName), ts.createIdentifier(typeKind) ), undefined ), ts.createLiteralTypeNode(ts.createStringLiteral(identifier)) ), undefined) type RootGetters = { "counter/double": COUNTER.G["double"]; "counter/expo": COUNTER.G["expo"]; "todos/todosCount": TODOS.G["todosCount"]; "todos/doneCount": TODOS.G["doneCount"]; };
  66. 4. ts.PropertySignature ts.createStringLiteral ts.createPropertySignature( undefined, ts.createStringLiteral(`${typeFile.namespace}/${identifier}`), undefined, ts.createIndexedAccessTypeNode( ts.createTypeReferenceNode( ts.createQualifiedName(

    ts.createIdentifier(typeFile.moduleName), ts.createIdentifier(typeKind) ), undefined ), ts.createLiteralTypeNode(ts.createStringLiteral(identifier)) ), undefined) type RootGetters = { "counter/double": COUNTER.G["double"]; "counter/expo": COUNTER.G["expo"]; "todos/todosCount": TODOS.G["todosCount"]; "todos/doneCount": TODOS.G["doneCount"]; }; String Literal の生成!!
  67. 4. ts.PropertySignature ts.createQualifiedName ts.createPropertySignature( undefined, ts.createStringLiteral(`${typeFile.namespace}/${identifier}`), undefined, ts.createIndexedAccessTypeNode( ts.createTypeReferenceNode( ts.createQualifiedName(

    ts.createIdentifier(typeFile.moduleName), ts.createIdentifier(typeKind) ), undefined ), ts.createLiteralTypeNode(ts.createStringLiteral(identifier)) ), undefined) type RootGetters = { "counter/double": COUNTER.G["double"]; "counter/expo": COUNTER.G["expo"]; "todos/todosCount": TODOS.G["todosCount"]; "todos/doneCount": TODOS.G["doneCount"]; };
  68. 4. ts.PropertySignature ts.createIndexedAccessTypeNode ts.createPropertySignature( undefined, ts.createStringLiteral(`${typeFile.namespace}/${identifier}`), undefined, ts.createIndexedAccessTypeNode( ts.createTypeReferenceNode( ts.createQualifiedName(

    ts.createIdentifier(typeFile.moduleName), ts.createIdentifier(typeKind) ), undefined ), ts.createLiteralTypeNode(ts.createStringLiteral(identifier)) ), undefined) type RootGetters = { "counter/double": COUNTER.G["double"]; "counter/expo": COUNTER.G["expo"]; "todos/todosCount": TODOS.G["todosCount"]; "todos/doneCount": TODOS.G["doneCount"]; };
  69. 4. ts.PropertySignature プロパティシグネチャを生成 ts.createPropertySignature( undefined, ts.createStringLiteral(`${typeFile.namespace}/${identifier}`), undefined, ts.createIndexedAccessTypeNode( ts.createTypeReferenceNode( ts.createQualifiedName(

    ts.createIdentifier(typeFile.moduleName), ts.createIdentifier(typeKind) ), undefined ), ts.createLiteralTypeNode(ts.createStringLiteral(identifier)) ), undefined) type RootGetters = { "counter/double": COUNTER.G["double"]; "counter/expo": COUNTER.G["expo"]; "todos/todosCount": TODOS.G["todosCount"]; "todos/doneCount": TODOS.G["doneCount"]; };
  70. 5. ts.TypeAliasDeclaration 生成したプロパティシグネチャ配列 type RootGetters = { "counter/double": COUNTER.G["double"]; "counter/expo":

    COUNTER.G["expo"]; "todos/todosCount": TODOS.G["todosCount"]; "todos/doneCount": TODOS.G["doneCount"]; }; ts.createTypeAliasDeclaration( undefined, undefined, ts.createIdentifier(identifier), undefined, ts.createTypeLiteralNode(typeElements) )
  71. 5. ts.TypeAliasDeclaration ts.createTypeLiteralNode type RootGetters = { "counter/double": COUNTER.G["double"]; "counter/expo":

    COUNTER.G["expo"]; "todos/todosCount": TODOS.G["todosCount"]; "todos/doneCount": TODOS.G["doneCount"]; }; ts.createTypeAliasDeclaration( undefined, undefined, ts.createIdentifier(identifier), undefined, ts.createTypeLiteralNode(typeElements) )
  72. 5. ts.TypeAliasDeclaration ts.createIdentifier type RootGetters = { "counter/double": COUNTER.G["double"]; "counter/expo":

    COUNTER.G["expo"]; "todos/todosCount": TODOS.G["todosCount"]; "todos/doneCount": TODOS.G["doneCount"]; }; ts.createTypeAliasDeclaration( undefined, undefined, ts.createIdentifier(identifier), undefined, ts.createTypeLiteralNode(typeElements) )
  73. 5. ts.TypeAliasDeclaration TypeAliasDeclaration を生成 type RootGetters = { "counter/double": COUNTER.G["double"];

    "counter/expo": COUNTER.G["expo"]; "todos/todosCount": TODOS.G["todosCount"]; "todos/doneCount": TODOS.G["doneCount"]; }; ts.createTypeAliasDeclaration( undefined, undefined, ts.createIdentifier(identifier), undefined, ts.createTypeLiteralNode(typeElements) )
  74. 6. ts.NodeArray TypeAliasDeclaration の配列を生成 const printer = ts.createPrinter() const emptyFile

    = ts.createSourceFile('', '', ts.ScriptTarget.ES2015) return printer.printList( ts.ListFormat.MultiLine, ts.createNodeArray([ ...importModules(typeFiles), importByLiteral('vuex'), declareModule('vuex', [ treeTypeAliasDeclaration(program, fileTree, 'RootState', 'S'), mapTypeAliasDeclaration(program, typeFiles, 'RootGetters', 'G'), mapTypeAliasDeclaration(program, typeFiles, 'RootMutations', 'M'), mapTypeAliasDeclaration(program, typeFiles, 'RootActions', 'A') ]) ]), emptyFile )
  75. 6. ts.NodeArray module declaration space を生成 const printer = ts.createPrinter()

    const emptyFile = ts.createSourceFile('', '', ts.ScriptTarget.ES2015) return printer.printList( ts.ListFormat.MultiLine, ts.createNodeArray([ ...importModules(typeFiles), importByLiteral('vuex'), declareModule('vuex', [ treeTypeAliasDeclaration(program, fileTree, 'RootState', 'S'), mapTypeAliasDeclaration(program, typeFiles, 'RootGetters', 'G'), mapTypeAliasDeclaration(program, typeFiles, 'RootMutations', 'M'), mapTypeAliasDeclaration(program, typeFiles, 'RootActions', 'A') ]) ]), emptyFile )
  76. 6. ts.NodeArray import module 構文 を生成 const printer = ts.createPrinter()

    const emptyFile = ts.createSourceFile('', '', ts.ScriptTarget.ES2015) return printer.printList( ts.ListFormat.MultiLine, ts.createNodeArray([ ...importModules(typeFiles), importByLiteral('vuex'), declareModule('vuex', [ treeTypeAliasDeclaration(program, fileTree, 'RootState', 'S'), mapTypeAliasDeclaration(program, typeFiles, 'RootGetters', 'G'), mapTypeAliasDeclaration(program, typeFiles, 'RootMutations', 'M'), mapTypeAliasDeclaration(program, typeFiles, 'RootActions', 'A') ]) ]), emptyFile )
  77. 6. ts.NodeArray 変換した AST が完成 const printer = ts.createPrinter() const

    emptyFile = ts.createSourceFile('', '', ts.ScriptTarget.ES2015) return printer.printList( ts.ListFormat.MultiLine, ts.createNodeArray([ ...importModules(typeFiles), importByLiteral('vuex'), declareModule('vuex', [ treeTypeAliasDeclaration(program, fileTree, 'RootState', 'S'), mapTypeAliasDeclaration(program, typeFiles, 'RootGetters', 'G'), mapTypeAliasDeclaration(program, typeFiles, 'RootMutations', 'M'), mapTypeAliasDeclaration(program, typeFiles, 'RootActions', 'A') ]) ]), emptyFile )
  78. 7. ts.Printer 文字列に変換 const printer = ts.createPrinter() const emptyFile =

    ts.createSourceFile('', '', ts.ScriptTarget.ES2015) return printer.printList( ts.ListFormat.MultiLine, ts.createNodeArray([ ...importModules(typeFiles), importByLiteral('vuex'), declareModule('vuex', [ treeTypeAliasDeclaration(program, fileTree, 'RootState', 'S'), mapTypeAliasDeclaration(program, typeFiles, 'RootGetters', 'G'), mapTypeAliasDeclaration(program, typeFiles, 'RootMutations', 'M'), mapTypeAliasDeclaration(program, typeFiles, 'RootActions', 'A') ]) ]), emptyFile )
  79. 8. fs.writeFileSync ファイルに書き込む import * as fs from 'fs' export

    default (dir: string, fileName: string, code: string) => { if (!fs.existsSync(dir)) { fs.mkdirSync(dir) } fs.writeFileSync(`${dir}${fileName}.ts`, code) }
  80. import * as COUNTER from "〜/store/counter/type"; import * as TODOS

    from "〜/store/todos/type"; import "vuex"; declare module "vuex" { type RootState = { counter: { count: COUNTER.S["count"]; }; todos: { todos: TODOS.S["todos"]; }; }; type RootGetters = { "counter/double": COUNTER.G["double"]; "counter/expo2": COUNTER.G["expo2"]; "counter/expo": COUNTER.G["expo"]; "todos/todosCount": TODOS.G["todosCount"]; "todos/doneCount": TODOS.G["doneCount"]; }; type RootMutations = { "counter/setCount": COUNTER.M["setCount"]; "counter/multi": COUNTER.M["multi"]; "counter/increment": COUNTER.M["increment"]; "counter/decrement": COUNTER.M["decrement"]; "todos/addTodo": TODOS.M["addTodo"]; "todos/doneTodo": TODOS.M["doneTodo"]; }; type RootActions = { "counter/asyncSetCount": COUNTER.A["asyncSetCount"]; "counter/asyncMulti": COUNTER.A["asyncMulti"]; "counter/asyncIncrement": COUNTER.A["asyncIncrement"]; "counter/asyncDecrement": COUNTER.A["asyncDecrement"]; "todos/asyncAddTodo": TODOS.A["asyncAddTodo"]; "todos/asyncDoneTodo": TODOS.A["asyncDoneTodo"]; }; } 自動で出力される メタ型定義 import 'vuex' declare module 'vuex' { type Getters<S, G> = { [K in keyof G]: ( state: S, getters: G, rootState: RootState, rootGetters: RootGetters ) => G[K] } type Mutations<S, M> = { [K in keyof M]: (state: S, payload: M[K]) => void } type ExCommit<M> = <T extends keyof M>(type: T, payload?: M[T]) => void type ExDispatch<A> = <T extends keyof A>(type: T, payload?: A[T]) => any type ExActionContext<S, A, G, M> = { commit: ExCommit<M> dispatch: ExDispatch<A> state: S getters: G rootState: RootState rootGetters: RootGetters } type Actions<S, A, G = {}, M = {}> = { [K in keyof A]: (ctx: ExActionContext<S, A, G, M>, payload: A[K]) => any } interface ExStore extends Store<RootState> { getters: RootGetters commit: ExCommit<RootMutations> dispatch: ExDispatch<RootActions> } type StoreContext = ExActionContext< RootState, RootActions, RootGetters, RootMutations > }
  81. import * as COUNTER from "〜/store/counter/type"; import * as TODOS

    from "〜/store/todos/type"; import "vuex"; declare module "vuex" { type RootState = { counter: { count: COUNTER.S["count"]; }; todos: { todos: TODOS.S["todos"]; }; }; type RootGetters = { "counter/double": COUNTER.G["double"]; "counter/expo2": COUNTER.G["expo2"]; "counter/expo": COUNTER.G["expo"]; "todos/todosCount": TODOS.G["todosCount"]; "todos/doneCount": TODOS.G["doneCount"]; }; type RootMutations = { "counter/setCount": COUNTER.M["setCount"]; "counter/multi": COUNTER.M["multi"]; "counter/increment": COUNTER.M["increment"]; "counter/decrement": COUNTER.M["decrement"]; "todos/addTodo": TODOS.M["addTodo"]; "todos/doneTodo": TODOS.M["doneTodo"]; }; type RootActions = { "counter/asyncSetCount": COUNTER.A["asyncSetCount"]; "counter/asyncMulti": COUNTER.A["asyncMulti"]; "counter/asyncIncrement": COUNTER.A["asyncIncrement"]; "counter/asyncDecrement": COUNTER.A["asyncDecrement"]; "todos/asyncAddTodo": TODOS.A["asyncAddTodo"]; "todos/asyncDoneTodo": TODOS.A["asyncDoneTodo"]; }; } module declaration space に宣言しているため、 自動生成された型は 用意した型パズルに declaration merge される。 import 'vuex' declare module 'vuex' { type Getters<S, G> = { [K in keyof G]: ( state: S, getters: G, rootState: RootState, rootGetters: RootGetters ) => G[K] } type Mutations<S, M> = { [K in keyof M]: (state: S, payload: M[K]) => void } type ExCommit<M> = <T extends keyof M>(type: T, payload?: M[T]) => void type ExDispatch<A> = <T extends keyof A>(type: T, payload?: A[T]) => any type ExActionContext<S, A, G, M> = { commit: ExCommit<M> dispatch: ExDispatch<A> state: S getters: G rootState: RootState rootGetters: RootGetters } type Actions<S, A, G = {}, M = {}> = { [K in keyof A]: (ctx: ExActionContext<S, A, G, M>, payload: A[K]) => any } interface ExStore extends Store<RootState> { getters: RootGetters commit: ExCommit<RootMutations> dispatch: ExDispatch<RootActions> } type StoreContext = ExActionContext< RootState, RootActions, RootGetters, RootMutations > }
  82. 型推論に重要なのは、 「Literal Types 」 これを自由に創出できる Node.js x Compiler API。 import

    * as COUNTER from "〜/store/counter/type"; import * as TODOS from "〜/store/todos/type"; import "vuex"; declare module "vuex" { type RootState = { counter: { count: COUNTER.S["count"]; }; todos: { todos: TODOS.S["todos"]; }; }; type RootGetters = { "counter/double": COUNTER.G["double"]; "counter/expo2": COUNTER.G["expo2"]; "counter/expo": COUNTER.G["expo"]; "todos/todosCount": TODOS.G["todosCount"]; "todos/doneCount": TODOS.G["doneCount"]; }; type RootMutations = { "counter/setCount": COUNTER.M["setCount"]; "counter/multi": COUNTER.M["multi"]; "counter/increment": COUNTER.M["increment"]; "counter/decrement": COUNTER.M["decrement"]; "todos/addTodo": TODOS.M["addTodo"]; "todos/doneTodo": TODOS.M["doneTodo"]; }; type RootActions = { "counter/asyncSetCount": COUNTER.A["asyncSetCount"]; "counter/asyncMulti": COUNTER.A["asyncMulti"]; "counter/asyncIncrement": COUNTER.A["asyncIncrement"]; "counter/asyncDecrement": COUNTER.A["asyncDecrement"]; "todos/asyncAddTodo": TODOS.A["asyncAddTodo"]; "todos/asyncDoneTodo": TODOS.A["asyncDoneTodo"]; }; } import 'vuex' declare module 'vuex' { type Getters<S, G> = { [K in keyof G]: ( state: S, getters: G, rootState: RootState, rootGetters: RootGetters ) => G[K] } type Mutations<S, M> = { [K in keyof M]: (state: S, payload: M[K]) => void } type ExCommit<M> = <T extends keyof M>(type: T, payload?: M[T]) => void type ExDispatch<A> = <T extends keyof A>(type: T, payload?: A[T]) => any type ExActionContext<S, A, G, M> = { commit: ExCommit<M> dispatch: ExDispatch<A> state: S getters: G rootState: RootState rootGetters: RootGetters } type Actions<S, A, G = {}, M = {}> = { [K in keyof A]: (ctx: ExActionContext<S, A, G, M>, payload: A[K]) => any } interface ExStore extends Store<RootState> { getters: RootGetters commit: ExCommit<RootMutations> dispatch: ExDispatch<RootActions> } type StoreContext = ExActionContext< RootState, RootActions, RootGetters, RootMutations > }
  83. import * as COUNTER from "〜/store/counter/type"; import * as TODOS

    from "〜/store/todos/type"; import "vuex"; declare module "vuex" { type RootState = { counter: { count: COUNTER.S["count"]; }; todos: { todos: TODOS.S["todos"]; }; }; type RootGetters = { "counter/double": COUNTER.G["double"]; "counter/expo2": COUNTER.G["expo2"]; "counter/expo": COUNTER.G["expo"]; "todos/todosCount": TODOS.G["todosCount"]; "todos/doneCount": TODOS.G["doneCount"]; }; type RootMutations = { "counter/setCount": COUNTER.M["setCount"]; "counter/multi": COUNTER.M["multi"]; "counter/increment": COUNTER.M["increment"]; "counter/decrement": COUNTER.M["decrement"]; "todos/addTodo": TODOS.M["addTodo"]; "todos/doneTodo": TODOS.M["doneTodo"]; }; type RootActions = { "counter/asyncSetCount": COUNTER.A["asyncSetCount"]; "counter/asyncMulti": COUNTER.A["asyncMulti"]; "counter/asyncIncrement": COUNTER.A["asyncIncrement"]; "counter/asyncDecrement": COUNTER.A["asyncDecrement"]; "todos/asyncAddTodo": TODOS.A["asyncAddTodo"]; "todos/asyncDoneTodo": TODOS.A["asyncDoneTodo"]; }; } Compiler API には、 未解決の型課題を解く ポテンシャルがある。 import 'vuex' declare module 'vuex' { type Getters<S, G> = { [K in keyof G]: ( state: S, getters: G, rootState: RootState, rootGetters: RootGetters ) => G[K] } type Mutations<S, M> = { [K in keyof M]: (state: S, payload: M[K]) => void } type ExCommit<M> = <T extends keyof M>(type: T, payload?: M[T]) => void type ExDispatch<A> = <T extends keyof A>(type: T, payload?: A[T]) => any type ExActionContext<S, A, G, M> = { commit: ExCommit<M> dispatch: ExDispatch<A> state: S getters: G rootState: RootState rootGetters: RootGetters } type Actions<S, A, G = {}, M = {}> = { [K in keyof A]: (ctx: ExActionContext<S, A, G, M>, payload: A[K]) => any } interface ExStore extends Store<RootState> { getters: RootGetters commit: ExCommit<RootMutations> dispatch: ExDispatch<RootActions> } type StoreContext = ExActionContext< RootState, RootActions, RootGetters, RootMutations > } 創出 X 導出
  84. おさらい

  85. Getters ✅ state, ✅ getters, ✅ rootState, ✅ rootGetters 推論観点で現実的な「型パズル」を構築する

    export const getters: Getters<S, G> = { todosCount(state, getters, rootState, rootGetters) { ... }, doneCount(state, getters, rootState, rootGetters) { ... } } type Getters<S, G> = { [K in keyof G]: ( state: S, getters: G, rootState: RootState, rootGetters: RootGetters ) => G[K] } export type S = { todos: Todo[] } export type G = { todosCount: number doneCount: number } export type RG = { 'todos/todosCount': G['todosCount'] 'todos/doneCount': G['doneCount'] } type RootState = { todos: Todos.S counter: Counter.S } type RootGetters = Todos.RG & Counter.RG
  86. Getters ✅ state, ✅ getters, ✅ rootState, ✅ rootGetters Compiler

    API に委ね「メタ型定義」を創出する export const getters: Getters<S, G> = { todosCount(state, getters, rootState, rootGetters) { ... }, doneCount(state, getters, rootState, rootGetters) { ... } } type Getters<S, G> = { [K in keyof G]: ( state: S, getters: G, rootState: RootState, rootGetters: RootGetters ) => G[K] } export type S = { todos: Todo[] } export type G = { todosCount: number doneCount: number } export type RG = { 'todos/todosCount': G['todosCount'] 'todos/doneCount': G['doneCount'] } type RootState = { todos: Todos.S counter: Counter.S } type RootGetters = Todos.RG & Counter.RG
  87. Getters ✅ state, ✅ getters, ✅ rootState, ✅ rootGetters 最後まで残ったメタ型定義…

    export const getters: Getters<S, G> = { todosCount(state, getters, rootState, rootGetters) { ... }, doneCount(state, getters, rootState, rootGetters) { ... } } type Getters<S, G> = { [K in keyof G]: ( state: S, getters: G, rootState: RootState, rootGetters: RootGetters ) => G[K] } export type S = { todos: Todo[] } export type G = { todosCount: number doneCount: number } export type RG = { 'todos/todosCount': G['todosCount'] 'todos/doneCount': G['doneCount'] } type RootState = { todos: Todos.S counter: Counter.S } type RootGetters = Todos.RG & Counter.RG
  88. AST 使うなら、それ不要では…?

  89. 誰もが「なんとなく」型を付与し 推論がまわりきるゴールまであと少し…

  90. To Be Continued ... @ Vue Fes Japan 2019

  91. Appendix 謝辞:DEMO 作成にあたり参考にさせて頂いた資料・ツール ▪ talks.leko.jp: https://talks.leko.jp/ ▪ TypeScript Compiler API

    の基本的な使い方: https://katashin.info/2018/02/24/221 ▪ Using the Compiler API: https://github.com/microsoft/TypeScript/wiki/Using-the-Compiler-API ▪ TypeScript AST Viewer: https://ts-ast-viewer.com/