Share
Embed
Meta Library VS Meta Type Definitions
TypeScript Meetup #2 https://typescript-jp.connpass.com/event/135033/
Takepepe
July 10, 2019
Transcript
91 slides
-
About Me ▪ Takefumi Yoshii / @Takepepe ▪ DeNA /
DeSC Healthcare ▪ Frontend Engineer ▪ 「型の強化書・実践 TypeScript」著者 -
Agenda 掲題の「Meta Library」は Vue.js でお馴染みの「Vuex」のこと。 ▪ 5分でわかる Vuex の概要と型課題 ▪
推論観点で紐解く Vuex に必要な型定義 ▪ TypeScript Compiler API で壁を超える -
Vuex - State management - ▪ 状態管理ライブラリの Vuex。Nuxt.js は標準で入る。 store
├── counter │ └── index.ts └── todos └── index.ts -
Vuex - State management - ▪ 関心の境界で Module を定義 store
├── counter │ └── index.ts └── todos └── index.ts -
Vuex - State management - ▪ Module は state を保持
store ├── counter │ └── index.ts └── todos └── index.ts store ├── counter │ └── { count: number } └── todos └── { todos: Todo[] } -
Vuex - State management - ▪ todos の実装を確認していきます store ├──
counter │ └── index.ts └── todos └── index.ts store ├── counter │ └── { count: number } └── todos └── { todos: Todo[] } -
Vuex - Inside Module - ▪ Module が保持する state ファクトリ関数
export const state = () => ({ todos: [] }) EX : store/todos/index.ts -
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 -
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 -
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 -
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 -
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 -
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 -
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 -
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 -
Vuex - Literal Reference - ▪ getter 関数の文字列参照(ツリー構造に起因) store ├──
counter │ └── index.ts └── todos └── index.ts store ├── counter │ ├── "counter/double" │ └── "counter/expo" └── todos ├── "todos/todosCount" └── "todos/doneCount" -
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 関数は、関数ではなく値として見える -
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 -
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 -
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」という状態を変更する関数群 -
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」と呼ばれる -
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 -
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 -
Vuex - Type Issues - ▪ 勘所が any に落ちている ▪
文字列参照と payload の「組」が担保できていない ▪ Generics による補正も効かない ▪ Module 名を変更しようものなら、影響範囲が甚大 -
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[] } -
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[] } -
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[] } -
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 = { } -
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 } -
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 関数は外からは値として見える -
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 関数は期待型を返す実装をしなければならない -
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 = { } -
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 } -
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 } -
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 } -
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 = {} -
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 -
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 -
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 -
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 -
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 -
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 -
1. Watch Store Dir 型構築に必要な情報を収集 store ├── counter │ ├──
index.ts │ └── type.ts ├── index.ts ├── todos │ ├── index.ts │ └── type.ts └── type.ts -
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' } ] -
1. Watch Store Dir 普通の Node.js コード // 定義対象ディレクトリから、定義対象ファイル一覧を取得 const
[typeFiles, fileTree] = await getTypeDefinitions(storeDir, 'type.ts') -
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 ファイルパスの文字列配列 -
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)) ) } -
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 で絞り込む -
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 の型宣言名称 -
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)) ) } -
生成したい型定義 type RootGetters = { "counter/double": COUNTER.G["double"]; "counter/expo": COUNTER.G["expo"]; "todos/todosCount":
TODOS.G["todosCount"]; "todos/doneCount": TODOS.G["doneCount"]; }; -
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 -
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"]; }; -
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 の生成!! -
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"]; }; -
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"]; }; -
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"]; }; -
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) ) -
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) ) -
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) ) -
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) ) -
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 ) -
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 ) -
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 ) -
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 ) -
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 ) -
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) } -
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 > } -
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 > } -
型推論に重要なのは、 「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 > } -
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 導出 -
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 -
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 -
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 -
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/