Slide 1

Slide 1 text

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

Slide 2

Slide 2 text

About Me ■ Takefumi Yoshii / @Takepepe ■ DeNA / DeSC Healthcare ■ Frontend Engineer ■ 「型の強化書・実践 TypeScript」著者

Slide 3

Slide 3 text

実践 TypeScript 発売から一週間で増刷確定しました! Amazon「ITコンピュータ・IT 関連」で 干し芋のランキング1位獲得 (一時的)

Slide 4

Slide 4 text

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

Slide 5

Slide 5 text

Agenda 掲題の「Meta Library」は Vue.js でお馴染みの「Vuex」のこと。 ■ 5分でわかる Vuex の概要と型課題 ■ 推論観点で紐解く Vuex に必要な型定義 ■ TypeScript Compiler API で壁を超える

Slide 6

Slide 6 text

5分でわかる Vuex の概要と型課題

Slide 7

Slide 7 text

Vuex - State management - ■ 状態管理ライブラリの Vuex。Nuxt.js は標準で入る。 store ├── counter │ └── index.ts └── todos └── index.ts

Slide 8

Slide 8 text

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

Slide 9

Slide 9 text

Vuex - State management - ■ Module は state を保持 store ├── counter │ └── index.ts └── todos └── index.ts store ├── counter │ └── { count: number } └── todos └── { todos: Todo[] }

Slide 10

Slide 10 text

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

Slide 11

Slide 11 text

Vuex - Inside Module - ■ Module が保持する state ファクトリ関数 export const state = () => ({ todos: [] }) EX : store/todos/index.ts

Slide 12

Slide 12 text

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

Slide 13

Slide 13 text

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

Slide 14

Slide 14 text

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

Slide 15

Slide 15 text

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

Slide 16

Slide 16 text

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

Slide 17

Slide 17 text

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

Slide 18

Slide 18 text

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

Slide 19

Slide 19 text

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

Slide 20

Slide 20 text

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

Slide 21

Slide 21 text

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 関数は、関数ではなく値として見える

Slide 22

Slide 22 text

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

Slide 23

Slide 23 text

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

Slide 24

Slide 24 text

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」という状態を変更する関数群

Slide 25

Slide 25 text

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」と呼ばれる

Slide 26

Slide 26 text

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

Slide 27

Slide 27 text

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

Slide 28

Slide 28 text

Vuex - Type Issues - ■ 勘所が any に落ちている ■ 文字列参照と payload の「組」が担保できていない ■ Generics による補正も効かない ■ Module 名を変更しようものなら、影響範囲が甚大

Slide 29

Slide 29 text

公式型定義では無理がある。 これはまずい、何とかしたい。

Slide 30

Slide 30 text

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

Slide 31

Slide 31 text

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

Slide 32

Slide 32 text

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

Slide 33

Slide 33 text

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

Slide 34

Slide 34 text

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

Slide 35

Slide 35 text

Getters ✅ state, ❌ getters, ❌ rootState, ❌ rootGetters getter 関数のメタ型定義「 type G 」 export const getters: Getters = { todosCount(state, getters, rootState, rootGetters) { ... }, doneCount(state, getters, rootState, rootGetters) { ... } } type Getters = { [k: string]: (state: S, getters: G) => unknown } export type S = { todos: Todo[] } export type G = { }

Slide 36

Slide 36 text

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

Slide 37

Slide 37 text

Getters ✅ state, ❌ getters, ❌ rootState, ❌ rootGetters 外部参照に向けた「期待型」を定義 export const getters: Getters = { todosCount(state, getters, rootState, rootGetters) { ... }, doneCount(state, getters, rootState, rootGetters) { ... } } type Getters = { [k: string]: (state: S, getters: G) => unknown } export type S = { todos: Todo[] } export type G = { todosCount: number doneCount: number } getter 関数は外からは値として見える

Slide 38

Slide 38 text

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

Slide 39

Slide 39 text

Getters ✅ state, ✅ getters, ❌ rootState, ❌ rootGetters ツリー構造に則した「 type RootState 」 export const getters: Getters = { todosCount(state, getters, rootState, rootGetters) { ... }, doneCount(state, getters, rootState, rootGetters) { ... } } type Getters = { [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 = { }

Slide 40

Slide 40 text

Getters ✅ state, ✅ getters, ✅ rootState, ❌ rootGetters 型を import して組み立てる export const getters: Getters = { todosCount(state, getters, rootState, rootGetters) { ... }, doneCount(state, getters, rootState, rootGetters) { ... } } type Getters = { [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 }

Slide 41

Slide 41 text

Getters ✅ state, ✅ getters, ✅ rootState, ❌ rootGetters 文字列参照のメタ型定義「 type RG 」 export const getters: Getters = { todosCount(state, getters, rootState, rootGetters) { ... }, doneCount(state, getters, rootState, rootGetters) { ... } } type Getters = { [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 }

Slide 42

Slide 42 text

Getters ✅ state, ✅ getters, ✅ rootState, ❌ rootGetters 文字列参照に「Indexed Access Types」でマッピング export const getters: Getters = { todosCount(state, getters, rootState, rootGetters) { ... }, doneCount(state, getters, rootState, rootGetters) { ... } } type Getters = { [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 }

Slide 43

Slide 43 text

Getters ✅ state, ✅ getters, ✅ rootState, ❌ rootGetters 文字列参照を集約した「 type RootGetters 」 export const getters: Getters = { todosCount(state, getters, rootState, rootGetters) { ... }, doneCount(state, getters, rootState, rootGetters) { ... } } type Getters = { [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 = {}

Slide 44

Slide 44 text

Getters ✅ state, ✅ getters, ✅ rootState, ✅ rootGetters 型を import して組み立てる export const getters: Getters = { todosCount(state, getters, rootState, rootGetters) { ... }, doneCount(state, getters, rootState, rootGetters) { ... } } type Getters = { [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

Slide 45

Slide 45 text

Getters ✅ state, ✅ getters, ✅ rootState, ✅ rootGetters All Green ! だがしかし… export const getters: Getters = { todosCount(state, getters, rootState, rootGetters) { ... }, doneCount(state, getters, rootState, rootGetters) { ... } } type Getters = { [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

Slide 46

Slide 46 text

Getters ✅ state, ✅ getters, ✅ rootState, ✅ rootGetters 定常開発で「メタ型定義」がこれだけ必要… export const getters: Getters = { todosCount(state, getters, rootState, rootGetters) { ... }, doneCount(state, getters, rootState, rootGetters) { ... } } type Getters = { [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

Slide 47

Slide 47 text

Getters ✅ state, ✅ getters, ✅ rootState, ✅ rootGetters ヒューマンエラーが防げず、スマートではない… export const getters: Getters = { todosCount(state, getters, rootState, rootGetters) { ... }, doneCount(state, getters, rootState, rootGetters) { ... } } type Getters = { [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

Slide 48

Slide 48 text

Getters ✅ state, ✅ getters, ✅ rootState, ✅ rootGetters ツリー構造に則した型推論は不可能では… export const getters: Getters = { todosCount(state, getters, rootState, rootGetters) { ... }, doneCount(state, getters, rootState, rootGetters) { ... } } type Getters = { [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

Slide 49

Slide 49 text

「実践TypeScript」にはここまで書いた。 これが残してきた課題。

Slide 50

Slide 50 text

引用 : 実践 TypeScript(p.305)

Slide 51

Slide 51 text

いやできる

Slide 52

Slide 52 text

TypeScript Compiler API で壁を超える

Slide 53

Slide 53 text

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

Slide 54

Slide 54 text

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

Slide 55

Slide 55 text

1. Watch Store Dir 型構築に必要な情報を収集 store ├── counter │ ├── index.ts │ └── type.ts ├── index.ts ├── todos │ ├── index.ts │ └── type.ts └── type.ts

Slide 56

Slide 56 text

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

Slide 57

Slide 57 text

1. Watch Store Dir 普通の Node.js コード // 定義対象ディレクトリから、定義対象ファイル一覧を取得 const [typeFiles, fileTree] = await getTypeDefinitions(storeDir, 'type.ts')

Slide 58

Slide 58 text

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 ファイルパスの文字列配列

Slide 59

Slide 59 text

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

Slide 60

Slide 60 text

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 で絞り込む

Slide 61

Slide 61 text

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 の型宣言名称

Slide 62

Slide 62 text

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

Slide 63

Slide 63 text

生成したい型定義 type RootGetters = { "counter/double": COUNTER.G["double"]; "counter/expo": COUNTER.G["expo"]; "todos/todosCount": TODOS.G["todosCount"]; "todos/doneCount": TODOS.G["doneCount"]; };

Slide 64

Slide 64 text

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

Slide 65

Slide 65 text

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"]; };

Slide 66

Slide 66 text

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 の生成!!

Slide 67

Slide 67 text

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"]; };

Slide 68

Slide 68 text

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"]; };

Slide 69

Slide 69 text

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"]; };

Slide 70

Slide 70 text

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

Slide 71

Slide 71 text

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

Slide 72

Slide 72 text

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

Slide 73

Slide 73 text

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

Slide 74

Slide 74 text

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 )

Slide 75

Slide 75 text

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 )

Slide 76

Slide 76 text

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 )

Slide 77

Slide 77 text

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 )

Slide 78

Slide 78 text

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 )

Slide 79

Slide 79 text

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

Slide 80

Slide 80 text

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 = { [K in keyof G]: ( state: S, getters: G, rootState: RootState, rootGetters: RootGetters ) => G[K] } type Mutations = { [K in keyof M]: (state: S, payload: M[K]) => void } type ExCommit = (type: T, payload?: M[T]) => void type ExDispatch = (type: T, payload?: A[T]) => any type ExActionContext = { commit: ExCommit dispatch: ExDispatch state: S getters: G rootState: RootState rootGetters: RootGetters } type Actions = { [K in keyof A]: (ctx: ExActionContext, payload: A[K]) => any } interface ExStore extends Store { getters: RootGetters commit: ExCommit dispatch: ExDispatch } type StoreContext = ExActionContext< RootState, RootActions, RootGetters, RootMutations > }

Slide 81

Slide 81 text

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 = { [K in keyof G]: ( state: S, getters: G, rootState: RootState, rootGetters: RootGetters ) => G[K] } type Mutations = { [K in keyof M]: (state: S, payload: M[K]) => void } type ExCommit = (type: T, payload?: M[T]) => void type ExDispatch = (type: T, payload?: A[T]) => any type ExActionContext = { commit: ExCommit dispatch: ExDispatch state: S getters: G rootState: RootState rootGetters: RootGetters } type Actions = { [K in keyof A]: (ctx: ExActionContext, payload: A[K]) => any } interface ExStore extends Store { getters: RootGetters commit: ExCommit dispatch: ExDispatch } type StoreContext = ExActionContext< RootState, RootActions, RootGetters, RootMutations > }

Slide 82

Slide 82 text

型推論に重要なのは、 「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 = { [K in keyof G]: ( state: S, getters: G, rootState: RootState, rootGetters: RootGetters ) => G[K] } type Mutations = { [K in keyof M]: (state: S, payload: M[K]) => void } type ExCommit = (type: T, payload?: M[T]) => void type ExDispatch = (type: T, payload?: A[T]) => any type ExActionContext = { commit: ExCommit dispatch: ExDispatch state: S getters: G rootState: RootState rootGetters: RootGetters } type Actions = { [K in keyof A]: (ctx: ExActionContext, payload: A[K]) => any } interface ExStore extends Store { getters: RootGetters commit: ExCommit dispatch: ExDispatch } type StoreContext = ExActionContext< RootState, RootActions, RootGetters, RootMutations > }

Slide 83

Slide 83 text

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 = { [K in keyof G]: ( state: S, getters: G, rootState: RootState, rootGetters: RootGetters ) => G[K] } type Mutations = { [K in keyof M]: (state: S, payload: M[K]) => void } type ExCommit = (type: T, payload?: M[T]) => void type ExDispatch = (type: T, payload?: A[T]) => any type ExActionContext = { commit: ExCommit dispatch: ExDispatch state: S getters: G rootState: RootState rootGetters: RootGetters } type Actions = { [K in keyof A]: (ctx: ExActionContext, payload: A[K]) => any } interface ExStore extends Store { getters: RootGetters commit: ExCommit dispatch: ExDispatch } type StoreContext = ExActionContext< RootState, RootActions, RootGetters, RootMutations > } 創出 X 導出

Slide 84

Slide 84 text

おさらい

Slide 85

Slide 85 text

Getters ✅ state, ✅ getters, ✅ rootState, ✅ rootGetters 推論観点で現実的な「型パズル」を構築する export const getters: Getters = { todosCount(state, getters, rootState, rootGetters) { ... }, doneCount(state, getters, rootState, rootGetters) { ... } } type Getters = { [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

Slide 86

Slide 86 text

Getters ✅ state, ✅ getters, ✅ rootState, ✅ rootGetters Compiler API に委ね「メタ型定義」を創出する export const getters: Getters = { todosCount(state, getters, rootState, rootGetters) { ... }, doneCount(state, getters, rootState, rootGetters) { ... } } type Getters = { [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

Slide 87

Slide 87 text

Getters ✅ state, ✅ getters, ✅ rootState, ✅ rootGetters 最後まで残ったメタ型定義… export const getters: Getters = { todosCount(state, getters, rootState, rootGetters) { ... }, doneCount(state, getters, rootState, rootGetters) { ... } } type Getters = { [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

Slide 88

Slide 88 text

AST 使うなら、それ不要では…?

Slide 89

Slide 89 text

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

Slide 90

Slide 90 text

To Be Continued ... @ Vue Fes Japan 2019

Slide 91

Slide 91 text

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/