xingbofeng.github.io
xingbofeng.github.io copied to clipboard
TypeScript 4.1 类型模板字符串实现Vuex的store.commit和store.dispatch类型判断
本文是在掘金的这篇文章(TS 4.1 新特性实现 Vuex 无限层级命名空间的 dispatch 类型推断。)的基础上进一步的实现,在阅读本文之前,可以先到掘金看看这篇文章。
TypeScript 4.1 类型模板字符串
首先在项目中安装 TypeScript 4.1 Beta 版本:
npm install typescript@beta --save-dev
可以在任何地方使用类型模板字符串:
1、直接使用
type World = "world";
type Greeting = `hello ${World}`;
上面的例子等价于:
type World = "world";
type Greeting = "hello world";
2、在类型映射中使用
type Options = {
[K in "noImplicitAny" | "strictNullChecks" | "strictFunctionTypes"]?: boolean
};
上面的例子等价于:
type Options = {
noImplicitAny?: boolean,
strictNullChecks?: boolean,
strictFunctionTypes?: boolean
};
3、用于联合类型
type Color = "red" | "blue";
type Quantity = "one" | "two";
type SeussFish = `${Quantity | Color} fish`;
上面的例子等价于:
type SeussFish = "one fish" | "two fish" | "red fish" | "blue fish";
4、常见的例子:用于事件定义
假设有一个函数makeWatchedObject
,用于遍历一个对象,生成一个格式相同的对象,但是每次改变此对象属性时会跑出对应的事件,使用一个新的on
方法来检测属性的更改:
let person = makeWatchedObject({
firstName: "Homer",
age: 42,
location: "Springfield",
});
person.on("firstNameChanged", () => {
console.log(`firstName was changed!`);
});
对于需要监听的事件名,可以用类型模板字符串实现:
type PropEventSource<T> = {
on<K extends string & keyof T>
(eventName: `${K}Changed`, callback: (newValue: T[K]) => void ): void;
};
declare function makeWatchedObject<T>(obj: T): T & PropEventSource<T>;
let person = makeWatchedObject({
firstName: "Homer",
age: 42,
location: "Springfield",
});
person.on("firstNameChanged", newName => {
console.log(`new name is ${newName.toUpperCase()}`);
});
person.on("ageChanged", newAge => {
if (newAge < 0) {
console.log("warning! negative age");
}
});
5、字符串转换工具函数
type EnthusiasticGreeting<T extends string> = `${Uppercase<T>}`
type HELLO = EnthusiasticGreeting<"hello">;
上面的例子等价于:
type HELLO = "HELLO";
除Uppercase
外,还有Lowercase
、Capitalize
、Uncapitalize
等工具函数
6、类型映射中使用 as 子句映射类型模板字符串生成的新key
type Options = {
noImplicitAny?: boolean,
strictNullChecks?: boolean,
strictFunctionTypes?: boolean
};
type ChangedOptions = {
[K in keyof Options as `Changed${Capitalize<keyof Options>}`]?: Options[K]
};
等价于:
type Options = {
noImplicitAny?: boolean,
strictNullChecks?: boolean,
strictFunctionTypes?: boolean
};
type ChangedOptions = {
ChangedNoImplicitAny?: boolean,
ChangedStrictNullChecks?: boolean,
ChangedStrictFunctionTypes?: boolean
};
问题描述
看下面这个例子,这是最常见的一个 Vuex 初始化方法:
const vuexOptions = {
state: {},
getters: {},
actions: {
async rootAction(actionsContext: ActionContext<{}, {}>, payload: string) { },
},
mutations: {
rootMutation(state: {}, context: number) { },
},
modules: {
home: {
actions: {
async homeAction(actionsContext: ActionContext<{}, {}>, homeContext: string) { },
},
mutations: {
homeMutation(state: {}, homeContext: number) { },
},
},
detail: {
actions: {
async detailAction(actionsContext: ActionContext<{}, {}>, detailContext: string) { },
},
mutations: {
detailMutation(state: {}, detailContext: number) { },
},
},
},
plugins: process.env.NODE_ENV === 'development' ? [createLogger()] : [],
};
const store = new Vuex.Store(vuexOptions);
export default store;
我们希望实现store.dispatch
时,可以出现智能提示和类型提示,如下图所示:
那么问题描述变为通过vuexOptions
,拿到一个action
名称到action
的payload
类型和dispatch
返回值的映射,等价于拿到如下类型:
type Actions = {
"home/homeAction": (homeContext: string) => Promise<void>;
"detail/detailAction": (detailContext: string) => Promise<void>;
rootAction: (payload: string) => Promise<void>;
}
type Mutations = {
"home/homeMutation": (homeContext: number) => void;
"detail/detailMutation": (detailContext: number) => void;
rootMutation: (context: number) => void;
}
通过上面的Actions
和Mutations
,我们可以很轻松地通过infer
拿到action
名称、payload
类型与dispatch
返回值类型,即推断actionFunction
的函数签名。
推断 actionFunction 的函数签名
我们知道在Vuex初始化时,action
一般是如下定义的:async homeAction(actionsContext: ActionContext<{}, {}>, homeContext: string) {}
,它的类型会被推断为:rootAction(actionsContext: ActionContext<{}, {}>, payload: string): Promise<void>
因此我们这里需要过滤掉第一个参数ActionContext
,通过infer
就可以实现:
type GetRestFuncType<T> = T extends (context: any, ...params: infer P) => infer R ? (...args: P) => R : never;
获取root模块的Actions和Mutations映射
type GetActionsTypes<Module> = Module extends { actions: infer M } ? {
[ActionKey in keyof M]: GetRestFuncType<M[ActionKey]>
} : never;
1、首先通过infer
关键字拿到单个模块的所有actions
的类型
2、再通过in
关键字对actions
的keys做类型映射
3、通过我们上面定义好的GetRestFuncType
拿到 actionFunction
的函数签名
获取任一模块的Actions和Mutations映射
但是,上述的实现并不完善,对于非root模块,例如detail
模块,例如想要dispatch
detail模块的 detailAction
时,就要用到模板字符串拼接为detail/detailAction
:
type AddPrefix<Keys, Prefix = ''> = `${Prefix & string}${Prefix extends '' ? '' : '/'}${Keys & string}`;
利用联合类型的自动展开,即可将GetActionsTypes
通用化到非root模块:
type GetActionsTypes<Module, ModuleName = ''> = Module extends { actions: infer M } ? {
[ActionKey in keyof M as AddPrefix<ActionKey, ModuleName>]: GetRestFuncType<M[ActionKey]>
} : never;
可以测试下如下代码:
type GetRestFuncType<T> = T extends (context: any, ...params: infer P) => infer R ? (...args: P) => R : never;
type AddPrefix<Keys, Prefix = ''> = `${Prefix & string}${Prefix extends '' ? '' : '/'}${Keys & string}`;
type GetActionsTypes<Module, ModuleName = ''> = Module extends { actions: infer M } ? {
[ActionKey in keyof M as AddPrefix<ActionKey, ModuleName>]: GetRestFuncType<M[ActionKey]>
} : never;
const module = {
actions: {
async homeAction(actionsContext: ActionContext<{}, {}>, homeContext: string) { },
}
};
type Actions = GetActionsTypes<typeof module, 'home'>;
拿到所有模块的Actions和Mutations映射
type GetModulesActionTypes<Modules> = {
[K in keyof Modules]: GetActionsTypes<Modules[K], K>
}[keyof Modules];
type GetSubModuleActionsTypes<Module> = Module extends { modules: infer SubModules } ? GetModulesActionTypes<SubModules> : never;
1、对于 root 模块而言,所有带命名空间的子模块都是放到modules
属性下面,同样可以通过infer
关键字拿到modules
下所有模块的类型
2、首先定义GetModulesActionTypes
,通过in
关键字对keyof Modules
做类型映射,这样将拿到所有定义到modules
的Actions
和Mutations
映射
3、然后定义GetSubModuleActionsTypes
,通过infer
拿到SubModules
类型,并通过GetModulesActionTypes
拿到Actions
和Mutations
映射
测试以下代码:
type GetRestFuncType<T> = T extends (context: any, ...params: infer P) => infer R ? (...args: P) => R : never;
type AddPrefix<Keys, Prefix = ''> = `${Prefix & string}${Prefix extends '' ? '' : '/'}${Keys & string}`;
type GetActionsTypes<Module, ModuleName = ''> = Module extends { actions: infer M } ? {
[ActionKey in keyof M as AddPrefix<ActionKey, ModuleName>]: GetRestFuncType<M[ActionKey]>
} : never;
type GetModulesActionTypes<Modules> = {
[K in keyof Modules]: GetActionsTypes<Modules[K], K>
}[keyof Modules];
type GetSubModuleActionsTypes<Module> = Module extends { modules: infer SubModules } ? GetModulesActionTypes<SubModules> : never;
const vuexOptions = {
state: {},
getters: {},
actions: {
async rootAction(actionsContext: ActionContext<{}, {}>, payload: string) { },
},
mutations: {
rootMutation(state: {}, context: number) { },
},
modules: {
home: {
actions: {
async homeAction(actionsContext: ActionContext<{}, {}>, homeContext: string) { },
},
mutations: {
homeMutation(state: {}, homeContext: number) { },
},
},
detail: {
actions: {
async detailAction(actionsContext: ActionContext<{}, {}>, detailContext: string) { },
},
mutations: {
detailMutation(state: {}, detailContext: number) { },
},
},
},
};
type AllActionsUnion = GetSubModuleActionsTypes<typeof vuexOptions>;
此时,AllActionsUnion
的类型为:
type AllActionsUnion = {
"home/homeAction": (homeContext: string) => Promise<void>;
} | {
"detail/detailAction": (detailContext: string) => Promise<void>;
}
显然我们希望得到的是这样的类型:
type AllActionsUnion = {
"home/homeAction": (homeContext: string) => Promise<void>;
"detail/detailAction": (detailContext: string) => Promise<void>;
}
这里我们需要将联合类型转化为交叉类型,我们可以利用Conditional Types in TypeScript
什么意思呢?对于T extends U ? X : Y
,当T
是联合类型时,例如A | B | C
,T extends U ? X : Y
会被自动展开为:(A extends U ? X : Y) | (B extends U ? X : Y) | (C extends U ? X : Y)
,利用这个特性,配合infer
关键字可以实现联合类型转交叉类型:
type UnionToIntersection<T> = (T extends any ? (k: T) => void : never) extends (k: infer I) => void ? I : never;
上面的UnionToIntersection
可以理解为:
当T
为A | B | C
时:
(T extends any ? (k: T) => void : never)
会被展开为(A extends any ? (k: A) => void : never) | (B extends any ? (k: B) => void : never) | (C extends any ? (k: C) => void : never)
,即(k: A) => void | (k: B) => void | (k: C) => void
,显然(k: A) => void | (k: B) => void | (k: C) => void
会被(k: A & B & C) => void
类型约束,那么使用infer
就可以解出A & B & C
了。
通过 actionFunction 的函数签名拿到 payload 和 返回值
1、拿到payload
:定义一个GetParam
泛型,通过infer
可推断出函数的第一个参数:
type GetParam<T extends (...args: any) => any> = T extends () => any ? undefined : T extends (arg: infer R) => any ? R : any;
2、拿到返回值:通过 TypeScript 内置的ReturnType
可实现
这里可以直接通过TypeScript的工具函数和ReturnType
拿到,其原理也不复杂,就是通过infer
关键字:
/**
* Obtain the return type of a function type
*/
type ReturnType<T extends (...args: any) => any> = T extends (...args: any) => infer R ? R : any;
完整的实现
通过上面的方法,就可以实现Vuex
的store.commit
和store.dispatch
类型判断,完整代码如下:
type GetRestFuncType<T> = T extends (context: any, ...params: infer P) => infer R ? (...args: P) => R : never;
type AddPrefix<Keys, Prefix = ''> = `${Prefix & string}${Prefix extends '' ? '' : '/'}${Keys & string}`;
type GetMutationsTypes<Module, ModuleName = ''> = Module extends { mutations: infer M } ? {
[MutationKey in keyof M as AddPrefix<MutationKey, ModuleName>]: GetRestFuncType<M[MutationKey]>
} : never;
type GetActionsTypes<Module, ModuleName = ''> = Module extends { actions: infer M } ? {
[ActionKey in keyof M as AddPrefix<ActionKey, ModuleName>]: GetRestFuncType<M[ActionKey]>
} : never;
type GetModulesMutationTypes<Modules> = {
[K in keyof Modules]: GetMutationsTypes<Modules[K], K>
}[keyof Modules];
type GetModulesActionTypes<Modules> = {
[K in keyof Modules]: GetActionsTypes<Modules[K], K>
}[keyof Modules];
type GetSubModuleMutationsTypes<Module> = Module extends { modules: infer SubModules } ? GetModulesMutationTypes<SubModules> : never;
type GetSubModuleActionsTypes<Module> = Module extends { modules: infer SubModules } ? GetModulesActionTypes<SubModules> : never;
type UnionToIntersection<T> = (T extends any ? (k: T) => void : never) extends (k: infer I) => void ? I : never;
type GetMutationsType<R> = UnionToIntersection<GetSubModuleMutationsTypes<R> | GetMutationsTypes<R>>;
type GetActionsType<R> = UnionToIntersection<GetSubModuleActionsTypes<R> | GetActionsTypes<R>>;
type GetParam<T> =
T extends () => any ? undefined :
T extends (arg: infer R) => any ? R : any;
type ReturnType<T> = T extends (...args: any) => infer R ? R : any;
type GetPayLoad<T, K extends keyof T> = GetParam<GetTypeOfKey<T, K>>;
type GetReturnType<T, K extends keyof T> = ReturnType<GetTypeOfKey<T, K>>;
const vuexOptions = {
state,
getters,
actions,
mutations,
modules: {
home,
detail,
},
plugins: process.env.NODE_ENV === 'development' ? [createLogger()] : [],
};
type Mutations = GetMutationsType<typeof vuexOptions>;
type Actions = GetActionsType<typeof vuexOptions>;
declare module 'vuex' {
export interface Commit {
<T extends keyof Mutations>(type: T, payload?: GetPayLoad<Mutations, T>, options?: CommitOptions): GetReturnType<Mutations, T>;
}
export interface Dispatch {
<T extends keyof Actions>(type: T, payload?: GetPayLoad<Actions, T>, options?: DispatchOptions): Promise<GetReturnType<Actions, T>>;
}
}
const store = new Vuex.Store<RootState>(vuexOptions);
小结
上面是笔者在Vuex中实践TypeScript模板字符串的总结,笔者也是顺手发布了一个npm 包,感兴趣的小伙伴看看vuex-typescript-commit-dispatch-prompt
使用方法
1、首先安装依赖,需要安装TypeScript 4.1 以上版本
npm install typescript@beta --save-dev
npm i vuex-typescript-commit-dispatch-prompt --save
2、在初始化store处引入vuex-typescript-commit-dispatch-prompt
,然后拓展vuex module 的类型定义
import Vuex from 'vuex';
import createLogger from 'vuex/dist/logger';
import { GetActionsType, GetMutationsType, GetPayLoad, GetReturnType } from 'vuex-typescript-commit-dispatch-prompt';
const vuexOptions = {
state,
getters,
actions,
mutations,
modules: {
home,
detail,
},
plugins: process.env.NODE_ENV === 'development' ? [createLogger()] : [],
};
type Mutations = GetMutationsType<typeof vuexOptions>;
type Actions = GetActionsType<typeof vuexOptions>;
declare module 'vuex' {
export interface Commit {
<T extends keyof Mutations>(type: T, payload?: GetPayLoad<Mutations, T>, options?: CommitOptions): GetReturnType<Mutations, T>;
}
export interface Dispatch {
<T extends keyof Actions>(type: T, payload?: GetPayLoad<Actions, T>, options?: DispatchOptions): Promise<GetReturnType<Actions, T>>;
}
}
const store = new Vuex.Store<RootState>(vuexOptions);
文章最后,笔者近期维护了一个公众号,用于分享前端新技术,欢迎大家到Counter的前端小站逛逛,一起关注前端的新技术发展。