$30 off During Our Annual Pro Sale. View Details »

Meta Library VS Meta Type Definitions

Takepepe
July 10, 2019

Meta Library VS Meta Type Definitions

Takepepe

July 10, 2019
Tweet

More Decks by Takepepe

Other Decks in Technology

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/