Vuex 4.x 利用Typescript实现智能提示效果

前言

在vue3的出现TS越来越普及,其实TS在早时候React都已经开始使用了,只不过是因为vue的市场大一些,而且React 18现在正式版也在今年3月29号正式发布了,在使用TS这方面显然是React更加成熟一些。

TS最明显的一个功能:智能提示,平时我们定义个对象的时候,要是这个对象里面只有几个对象还好,要是100个呢?你能全部把里面的key记住?反正我是记不住,还会回去把key复制一下,粘贴过来使用。这样是不是很麻烦,一开始写的就是,这个东西着实就很痛苦。譬如开发项目时候定义接口方法名的方法,久久不用一次,但是你隐约记得前面开头是什么,但记不全。TS是有一个很友好的提示方式

image.png

直接输入api就会给你提示这个对象里面的内容,是不是方便很多,但是在我们普通的js文件中是没有的

image.png

但以为TS的智能提示是无处不在的时候,在使用Vuex的时候,问题出现了

image.png

image.png

image.png

很显然并不能智能提示了,就这点体验而言Pinia要Vuex爽很多,这就是为什么Pinia会被大力推荐,其实Pinia的开发者是@posva,是Vuex团队的核心开发人员之一,未来Vuex5的提案中的灵感都是来源于Pinia

image.png

image.png

今天主要是将这个Vuex的智能提示搞一搞。开始之前先来回顾一下TS的几个知识点。

TypeScript 泛型

软件工程中,我们不仅要创建一致的定义良好的 API,同时也要考虑可重用性。 组件不仅能够支持当前的数据类型,同时也能支持未来的数据类型,这在创建大型系统时为你提供了十分灵活的功能。

在像 C# 和 Java 这样的语言中,可以使用泛型来创建可重用的组件,一个组件可以支持多种类型的数据。 这样用户就可以以自己的数据类型来使用组件。

设计泛型的关键目的是在成员之间提供有意义的约束,这些成员可以是:类的实例成员、类的方法、函数参数和函数返回值。

泛型(Generics)是允许同一个函数接受不同类型参数的一种模板。相比于使用 any 类型,使用泛型来创建可复用的组件要更好,因为泛型会保留参数类型。

泛型语法

对于刚接触 TypeScript 泛型的小伙伴来说,首次看到 <T> 语法会感到陌生。其实它没有什么特别,就像传递参数一样,我们传递了我们想要用于特定函数调用的类型。

image.png

参考上面的图片,当我们调用 identity<Number>(1)Number 类型就像参数 1 一样,它将在出现 T 的任何位置填充该类型。图中 <T> 内部的 T 被称为类型变量,它是我们希望传递给 identity 函数的类型占位符,同时它被分配给 value 参数用来代替它的类型:此时 T 充当的是类型,而不是特定的 Number 类型。

其中 T 代表 Type,在定义泛型时通常用作第一个类型变量名称。但实际上 T 可以用任何有效名称代替。除了 T 之外,以下是常见泛型变量代表的意思:

  • K(Key):表示对象中的键类型;
  • V(Value):表示对象中的值类型;
  • E(Element):表示元素类型。

其实并不是只能定义一个类型变量,我们可以引入希望定义的任何数量的类型变量。比如我们引入一个新的类型变量 U,用于扩展我们定义的 identity 函数:

image.png

除了为类型变量显式设定值之外,一种更常见的做法是使编译器自动选择这些类型,从而使代码更简洁。我们可以完全省略尖括号,比如:

function identity <T, U>(value: T, message: U) : T {
  console.log(message);
  return value;
}

console.log(identity(28, "张三"));
复制代码

对于上述代码,编译器足够聪明,能够知道我们的参数类型,并将它们赋值给 T 和 U,而不需要开发人员显式指定它们。

泛型接口

interface GenericIdentityFn<T> {
  (arg: T): T;
}
复制代码

泛型类

class GenericNumber<T> {
  zeroValue: T;
  add: (x: T, y: T) => T;
}

let myGenericNumber = new GenericNumber<number>();
myGenericNumber.zeroValue = 0;
myGenericNumber.add = function (x, y) {
  return x + y;
};
复制代码

泛型工具类型

为了方便开发者 TypeScript 内置了一些常用的工具类型,比如 Partial、Required、Readonly、Record 和 ReturnType 等。由于本次不是用到所有的Utility Types,这里只讲解使用到的,更多详情可以看TypeScript官网对Utility Types的解释,不过在具体介绍之前,我们得先介绍一些相关的基础知识,方便读者自行学习其它的工具类型。

1.typeof

在 TypeScript 中,typeof 操作符可以用来获取一个变量声明或对象的类型。

interface Person{
    age:number
    name:string
}

var obj:Person ={
    age:18,
    name:"杨贵妃"
} 

type Obj = typeof obj // -> Person

function toArray(x: number): Array<number> {
  return [x];
}

type Func = typeof toArray; // -> (x: number) => number[]
复制代码

2.keyof

keyof 操作符是在 TypeScript 2.1 版本引入的,该操作符可以用于获取某种类型的所有键,其返回类型是联合类型。

interface Person {
  name: string;
  age: number;
}

type K1 = keyof Person; // "name" | "age"
type K2 = keyof Person[]; // "length" | "toString" | "pop" | "push" | "concat" | "join" 
type K3 = keyof { [x: string]: Person };  // string | number

复制代码

在 TypeScript 中支持两种索引签名,数字索引和字符串索引:

interface StringArray {
  // 字符串索引 -> keyof StringArray => string | number
  [index: string]: string; 
}

interface StringArray1 {
  // 数字索引 -> keyof StringArray1 => number
  [index: number]: string;
}

复制代码

为了同时支持两种索引类型,就得要求数字索引的返回值必须是字符串索引返回值的子类。其中的原因就是当使用数值索引时,JavaScript 在执行索引操作时,会先把数值索引先转换为字符串索引。所以 keyof { [x: string]: Person } 的结果会返回 string | number

image.png

3.in

in 用来遍历枚举类型:

type Keys = "a" | "b" | "c"

type Obj =  {
  [p in Keys]: any
} // -> { a: any, b: any, c: any }
复制代码

4.extends

extends 操作符是在 TypeScript 2.8 版本引入的有条件类型,它能够表示非统一的类型。 有条件的类型会以一个条件表达式进行类型关系检测,从而在两种类型中选择其一:

T extends U ? X : Y
复制代码

上面的类型意思是,若T能够赋值给U,那么类型是X,否则为Y。更多详情可以看一下Typescript官网对extends的解释

有时候我们定义的泛型不想过于灵活或者说想继承某些类等,可以通过 extends 关键字添加泛型约束。

interface Lengthwise {
  length: number;
}

function loggingIdentity<T extends Lengthwise>(arg: T): T {
  console.log(arg.length);
  return arg;
}
复制代码

现在这个泛型函数被定义了约束,因此它不再是适用于任意类型:

loggingIdentity(3);  // Error, number doesn't have a .length property
复制代码

这时我们需要传入符合约束类型的值,必须包含必须的属性:

loggingIdentity({length: 10, value: 3});
复制代码

5.infer

现在在有条件类型的extends子语句中,允许出现infer声明,它会引入一个待推断的类型变量。 这个推断的类型变量可以在有条件类型的true分支中被引用。 允许出现多个同类型变量的infer

type ReturnType<T> = T extends (
  ...args: any[]
) => infer R ? R : any;
复制代码

以上代码中 infer R 就是声明一个变量来承载传入函数签名的返回值类型,简单说就是用它取到函数返回值的类型方便之后使用。

6.ReturnType

用于获得函数返回值的类型,譬如这样:

function aa(){

    const bb = {
        cc:"你好"
    }

    return bb
}

type funType = ReturnType<typeof aa>
复制代码

image.png

自动就能获取到了函数中返回值中的类型,是不是很强。

7.Partial

Partial<T> 的作用就是将某个类型里的属性全部变为可选项 ?

定义:

/**
 * node_modules/typescript/lib/lib.es5.d.ts
 * Make all properties in T optional
 */
type Partial<T> = {
  [P in keyof T]?: T[P];
};
复制代码

在以上代码中,首先通过 keyof T 拿到 T 的所有属性名,然后使用 in 进行遍历,将值赋给 P,最后通过 T[P] 取得相应的属性值。中间的 ? 号,用于将所有属性变为可选。

示例:

interface Todo {
  title: string;
  description: string;
}

function updateTodo(todo: Todo, fieldsToUpdate: Partial<Todo>) {
  return { ...todo, ...fieldsToUpdate };
}

const todo1 = {
  title: "Learn TS",
  description: "Learn TypeScript",
};

const todo2 = updateTodo(todo1, {
  description: "Learn TypeScript Enum",
});
复制代码

在上面的 updateTodo 方法中,我们利用 Partial<T> 工具类型,定义 fieldsToUpdate 的类型为 Partial<Todo>,即:

{
   title?: string | undefined;
   description?: string | undefined;
}
复制代码

接下来就开始今天的主题内容了,创建项目的过程就省略了,用vite搭建的。

创建store

这里主要是创建了三个模块,app、user、test,你可以分开写或者全写在一个ts里面写都没有啥问题的。

image.png

要使用我们创建好的store首先要在main.ts中引入

image.png

在页面中使用一下创建的store,在Vuex的官网中提供了一种组合hooks函数去获取ore实例,当然是建立在使用场景setup中的

image.png

拿到了在每个模块下面写的state后就可以证明,我们写成功了。

image.png

到最后一个步了,来输入智能提示的功能

创建utils.ts

曾今想利用vite的自动化搜索文件的功能,搜索到modules文件夹中所有导出的模块,就不需要一个个的去引入文件,这样很难受。webpack中是require.context,vite是Glob 导入又分为同步和异步。显然并没有成功,有兴趣的小伙伴可以尝试一下

新建一个utils.ts来做这个功能

获取到 getters和actions 结构类型

基本思路:

  • 首先利用typeof获取到modules的类型,
  • 再利用keyof 将modules对象中的key拿到,利用in遍历拿到的key
  • 利用条件判断的方式extends 和 infer去获取到每个getters对象或者actions对象中函数返回的类型,如果没有返回值就会unknown
  • 获取到的函数返回类型赋值给当前key的类型

代码如下:

/**
 * 拿到store下的modules
 */
import modules from "./modules"

/**
 * 获取到 getters和actions 结构类型
 * 首先利用typeof获取到modules的类型,
 * 再利用keyof 将modules对象中的key拿到,利用in遍历拿到的key
 * 利用条件判断的方式extends 和 infer去获取到每个getters对象或者actions对象中函数返回的类型,如果没有返回值就会unknown
 * 获取到的函数返回类型赋值给当前key的类型
 */

 type GetGetter<Moudle> = Moudle extends { getters: infer G } ? G : unknown;
 // 获取vuex 所有的getter模块
 type GetGetters<Modules> = {
   [K in keyof Modules]: GetGetter<Modules[K]>;
 };
 type ModuleGettes = GetGetters<typeof modules>;
 
 /* 获取到action 结构类型 */
 // 匹配到 单个module 下的 action
 type GetAction<Moudle> = Moudle extends { actions: infer A } ? A : unknown;
 // 获取vuex 所有的action模块
 type GetActions<Modules> = {
   [K in keyof Modules]: GetAction<Modules[K]>;
 };
 type ModuleActions = GetActions<typeof modules>;



export {}
复制代码

image.png

image.png

看到这样就成功了第一步了。

智能提示dispatch('user/getData')

基本思路:

  • 将上面获取到的ModuleGettesModuleActions进行处理
  • 譬如user模块中的getData方法,要变为"user/getData"
  • 跟上一步的思路还是很像的,都是利用了in、keyof、extends、infer
  • 还利用了typescript 4.1 新增加的语法 模板字符串语法的功能
  • 在ts类型中的&表示交集,两个类型都拥有的类型

代码如下:

/**
 * 将上面获取到的ModuleGettes和ModuleActions进行处理
 * 譬如user模块中的getData方法,要变为"user/getData"
 * 跟上一步的思路还是很像的,都是利用了in、keyof、extends、infer
 * 还利用了typescript 4.1 新增加的语法 模板字符串语法的功能
 * 在ts类型中的&表示交集,两个类型都拥有的类型
 */

/* Getter/Dispatch智能提示处理 */
// 由于传入的是 keyof 有可能是symbol | number,所以 Prefix & string 取其中的string
// typescript 4.1 新增加的语法  vuex xxx/xxx 终于可以有提示了   模板字符串语法 和 ES6字符串模板 是一样的
type AddPrefix<Prefix, Keys> = `${Prefix & string}/${Keys & string}`
type GetSpliceKey<Module, Key> = AddPrefix<Key, keyof Module>
type GetSpliceKeys<Modules> = {
    [K in keyof Modules]: GetSpliceKey<Modules[K], K>
}[keyof Modules]

type GetFunc<T, A, B> = T[A & keyof T][B & keyof T[A & keyof T]];

type GetSpliceObj<T> = {
    [K in GetSpliceKeys<T>]: K extends `${infer A}/${infer B}` ? GetFunc<T, A, B> : unknown
}

type ModulesGettes = GetSpliceObj<ModuleGettes>
type MoudlesActions = GetSpliceObj<ModuleActions>;
复制代码

image.png

image.png

从上面图片可以看出,getters中有着很多我们不需要的,需要将其除去

type Getters = {
    [K in keyof MoudlesGetters]:ReturnType<MoudlesGetters[K]>
}
复制代码

image.png

这样getters的就完结了,但是actions的还没有,在使用actions的使用我们是使用dispatch进行调用的,actions中的处理函数总是接受 context 作为第一个参数,payload 作为第二个参数(可选)。第二参数可有可无,看具体场景。

interface GetDispatch<T> {
    <K extends keyof T>(action: K, payload?: T[K]): Promise<unknown>
}
type Dispatch = GetDispatch<MoudlesActions>;
复制代码

完整代码

/**
 * 拿到store下的modules
 */
import modules from "./modules"

/**
 * 获取到 getters和actions 结构类型
 * 首先利用typeof获取到modules的类型,
 * 再利用keyof 将modules对象中的key拿到,利用in遍历拿到的key
 * 利用条件判断的方式extends 和 infer去获取到每个getters对象或者actions对象中函数返回的类型,如果没有返回值就会unknown
 * 获取到的函数返回类型赋值给当前key的类型
 */

type GetGetter<Moudle> = Moudle extends { getters: infer G } ? G : unknown;
// 获取vuex 所有的getter模块
type GetGetters<Modules> = {
    [K in keyof Modules]: GetGetter<Modules[K]>;
};
type ModuleGettes = GetGetters<typeof modules>;

/* 获取到action 结构类型 */
// 匹配到 单个module 下的 action
type GetAction<Moudle> = Moudle extends { actions: infer A } ? A : unknown;
// 获取vuex 所有的action模块
type GetActions<Modules> = {
    [K in keyof Modules]: GetAction<Modules[K]>;
};
type ModuleActions = GetActions<typeof modules>;

/**
 * 将上面获取到的ModuleGettes和ModuleActions进行处理
 * 譬如user模块中的getData方法,要变为"user/getData"
 * 跟上一步的思路还是很像的,都是利用了in、keyof、extends、infer
 * 还利用了typescript 4.1 新增加的语法 模板字符串语法的功能
 * 在ts类型中的&表示交集,两个类型都拥有的类型
 */

/* Getter/Dispatch智能提示处理 */
// 由于传入的是 keyof 有可能是symbol | number,所以 Prefix & string 取其中的string
// typescript 4.1 新增加的语法  vuex xxx/xxx 终于可以有提示了   模板字符串语法 和 ES6字符串模板 是一样的
type AddPrefix<Prefix, Keys> = `${Prefix & string}/${Keys & string}`
type GetSpliceKey<Module, Key> = AddPrefix<Key, keyof Module>
type GetSpliceKeys<Modules> = {
    [K in keyof Modules]: GetSpliceKey<Modules[K], K>
}[keyof Modules]

type GetFunc<T, A, B> = T[A & keyof T][B & keyof T[A & keyof T]];

type GetSpliceObj<T> = {
    [K in GetSpliceKeys<T>]: K extends `${infer A}/${infer B}` ? GetFunc<T, A, B> : unknown
}

type MoudlesGetters = GetSpliceObj<ModuleGettes>
type MoudlesActions = GetSpliceObj<ModuleActions>;

type Getters = {
    [K in keyof MoudlesGetters]: ReturnType<MoudlesGetters[K]>
}

interface GetDispatch<T> {
    <K extends keyof T>(action: K, payload?: T[K]): Promise<unknown>
}
type Dispatch = GetDispatch<MoudlesActions>;

export type { Getters, Dispatch }
export default Getters
复制代码

重写useStore

写完上面的就可以进行改变store的使用方式了,进行改写hooks函数useStore,新建一个文件夹hooks专门存放hooks函数。底下创建一个useStore.ts

代码如下:

import { useStore as baseUseStore } from "vuex"
import { State } from "../store"
import { Getters, Dispatch } from "../store/utils"

interface UseStore {
    state: State,
    getters: Getters,
    dispatch: Dispatch
}

const useStore = (): UseStore => {
    const store = baseUseStore<State>()
    const { state, getters, dispatch } = store
    return { state, getters, dispatch }
}

export { useStore }
export default useStore
复制代码

实践出真知

在页面中使用一下,看看效果

image.png

image.png

image.png

咦,神奇的事情发生了,为何什么会有这个提示的,这个就是TS的强大之处

总结

Typescript谁用谁知道,真的是爽的一匹

拓展hooks函数

Vue3 究竟好在哪里?(和 React Hook 的详细对比)

另外Vue有个Hooks库叫做VueUse,里面有着非常多的Hooks函数,非常的好用,谁用谁知道。

就譬如这个useResizeObserver可以直接监听元素大小的变化,爽的一匹

猜你喜欢

转载自juejin.im/post/7084156568110907400
今日推荐