前言

使用了ts最头疼的是什么,除了类型声明应该没有第二家了,那么在vue3中如何正确的声明ts类型,代表着我们踏出了认识vue3的第一步,这非常重要,所以为此水个文章,分享给有需要的人。

Volar 插件

一开始我对于Volar并没有太大的需要,因为一直使用的Vetur,而且这个插件刚出来时并不完善,各种视频up讲的那个一键分屏功能其实也并不好用,虽然是个很有意思的东西,但是没有那种非要使用它的点,所以当时的我怀着这么一个疑问?为什么要用Volar ?

现在我就通过两张图告诉你,它有多香!

我们在template里面写代码,绑定变量最烦的是什么,就是我们写了个对象,但是忘了它的属性有哪些啊,使用了Volar配合ts类型声明,就可以再template里面实现代码提示和类型提示,这特么不得吹爆啊,所以还在犹豫什么,用起来。

并且当你的属性或者变量不存在时,template中也会有对应的波浪线错误提示。

data的类型声明

vue3中数据的声明一般使用ref或者reactive;这两个方法都是使用的泛型定义的类型,默认情况下他会自动推断出你书写的数据。

typescript
复制代码
<script lang="ts"> import { defineComponent, reactive, ref } from "vue"; export default defineComponent({ setup() { const data = reactive({ name: "测试数据", age: 16, status: false, }); const data2 = ref(false); return { data, data2, }; }, }); </script>

但是有时候我们可能需要一个空数组来接受来自api的数据,此时泛型就无法推断出数据类型了,所以我们需要手动定义。

例:

typescript
复制代码
<script lang="ts"> import { defineComponent, reactive, ref } from "vue"; export default defineComponent({ setup() { type Item = { name: string; age: number; status: boolean; }; const data = reactive<Array<Item>>([]); return { data, }; }, }); </script>

ref也是同理,不会可以看下官方的这个文档,有具体的泛型源码参考:响应式核心

那么使用vue2的方式声明data,类型该怎么声明呢?

typescript
复制代码
<script lang="ts"> import { defineComponent } from "vue"; type Item = { name: string; age: number; status: boolean; }; export default defineComponent({ data() { return { data: [] as Array<Item>, }; }, }); </script>

我们可以通过as的方式进行断言处理,如果是静态的属性,其实ts也可以自行推断出来,可以不用自己手动断言类型。

补充:

如果你使用reactive,然后响应的值会被解包处理,那么可能通过泛型的方式传入的类型会出现问题,于是官方推荐我们使用这种形式的类型声明,当然你也可以默认就使用这种方式,这也是官方推荐的。

typescript
复制代码
<script lang="ts"> import { defineComponent, reactive } from "vue"; export default defineComponent({ setup() { type Item = { name: string; age: number; status: boolean; }; const data: Array<Item> = reactive([]); return { data, }; }, }); </script>

props的类型声明

vue3的props使用其实和vue2是一样的,只不过它对于setup组件式语法增加了一个新的定义方法defineProps,但其实它的定义格式还是一样的,都可以定义typerequired等等属性。

我们创建一个test组件,跟以往一样书写一个props属性,需要接收一个string类型的id值来进行显示

typescript
复制代码
<template> <div> {{ id }} </div> </template> <script lang="ts"> import { defineComponent } from "vue"; export default defineComponent({ props: { id: { type: String, required: true, }, }, }); </script>

父级调用:

typescript
复制代码
<template> <div> <test id="我是id" /> </div> </template> <script lang="ts"> import { defineComponent } from "vue"; import test from "./components/test.vue"; export default defineComponent({ components: { test, }, }); </script>

你会发现和以往并没有什么不同,但是,你我们props的type类型好像一直都是一种宽泛的定义,我们能不能更加精准的控制id的内容呢,比如我希望id的值是字符类型的1和2,不能有其它的值,能做到吗?

答案是可以的,我们可以这么写:

typescript
复制代码
<script lang="ts"> import { defineComponent } from "vue"; import type { PropType } from "vue"; export default defineComponent({ props: { id: { type: String as PropType<"1" | "2">, required: true, }, }, }); </script>

需要注意的是,vue3的props类型全部需要通过官方提供的PropType的泛型来实现,必须基于它,而且我们引入的时候需要注意,如果是一个类型声明,我们必须在import的时候加上type声明,以表示我们引入的是一个类型声明而不是一个变量。

此时我们再回到父级,你会发现报错了!

非常明确的给出了提示,非常棒。

下面是defineProps的用法:

当我们使用setup组件时,这个defineProps会自动引入,无需手动import

typescript
复制代码
<template> <div> {{ id }} </div> </template> <script lang="ts" setup> //定义props defineProps({ id: { type: String, required: true, }, }); </script>

精准控制:

typescript
复制代码
<script lang="ts" setup> import type { PropType } from "vue"; //定义props defineProps({ id: { type: String as PropType<"1" | "2">, required: true, }, }); </script>

computed的类型声明

computed其实可以看成是一个函数,函数的类型咋声明,它就咋声明。

typescript
复制代码
<template> <div>{{ data }}</div> </template> <script lang="ts"> import { defineComponent } from "vue"; export default defineComponent({ computed: { data(): string { return "数据"; }, }, }); </script>

除了返回值的类型声明,我们可以声明形参类型。

typescript
复制代码
<template> <div>{{ data }}</div> </template> <script lang="ts"> import { defineComponent } from "vue"; import type { RouteLocationNormalizedLoaded } from "vue-router"; export default defineComponent({ computed: { data({ $route }: { $route: RouteLocationNormalizedLoaded }): string | symbol { return $route.name || "暂无"; }, }, }); </script>

methods的类型声明

这个其实和computed是一样的,就不过多介绍了。

vuex的类型声明

以目前vuex的结构来说,很难有一个很好的静态类型推断,为啥?因为它的模块数据的调用是this.$store.state.模块名.xxx这样获取的,但是它的模块数据实际的注册却是通过modules对象挂载。不和根state对象在一起。

所以我们在类型声明的时候很难去通过声明一个根State类型来定义所有,必须给模块属性加上?.;否则就会导致最开始根state就校验不过去,因为根state的对象属性里面并不包含模块的属性,这些属性是通过modules后加上去的。

这也就导致我们在调用模块的属性的时候不得不也加上?.

typescript
复制代码
this.$store.state.模块名?.xxx

甚至我们使用!.让ts不再报undefined值的错误。

typescript
复制代码
this.$store.state.模块名!.xxx

为此我们也只能是增加调用state的类型提示,以及Mutation、Action的方法中state的自行推断和形参校验,其他就没有了。

getters也能自行推断出state,其他就没了,如果能接受,我们往下看。

this.$store类型推断

首先我先创建一个store目录,创建一个index.ts文件用于存放根state和vuex实例。

store/index.ts

typescript
复制代码
import { createStore } from "vuex"; export type State = { name: string; }; const store = createStore({ state: { name: "vuex", }, }); export default store;

main.ts挂载store

typescript
复制代码
import { createApp } from "vue"; import App from "./App.vue"; import store from "./store"; createApp(App).use(store).mount("#app");

此时vuex就初始化好了,我们需要创建一个全局声明模块,用于扩充vue的this对象,会用到vue提供的ComponentCustomProperties方法,文档地址:增加组件实例类型以支持自定义全局属性

在src目录下创建一个vuex.d.ts文件,写入以下内容:

typescript
复制代码
import { ComponentCustomProperties } from "vue"; import { Store } from "vuex"; import { State } from "./store"; declare module "vue" { interface ComponentCustomProperties { $store: Store<State>; } }

引入刚刚导出的State根类型声明,在这使用。

其实这个文件名也不一定非得叫vuex.d.ts,自行调整。

此时我们通过this.$store就能获得代码提示和类型推断。

useStore类型推断

为了 useStore 能正确返回类型化的 store,必须执行以下步骤:

  1. 定义类型化的 InjectionKey
  2. 将 store 安装到 Vue 应用时提供类型化的 InjectionKey
  3. 将类型化的 InjectionKey 传给 useStore 方法。

首先我们创建injection key:

store/index.ts

typescript
复制代码
import { createStore, Store } from "vuex"; import { InjectionKey } from "vue"; export type State = { name: string; }; export const key: InjectionKey<Store<State>> = Symbol(); const store = createStore({ state: { name: "vuex", }, }); export default store;

use(store)注册vuex的时候将key作为第二个参数传入:

typescript
复制代码
import { createApp } from "vue"; import App from "./App.vue"; import store, { key } from "./store"; createApp(App).use(store, key).mount("#app");

然后,当我们在使用useStore的时候,也需要传入key。

typescript
复制代码
<script lang="ts"> import { defineComponent } from "vue"; import { useStore } from "vuex"; import { key } from "./store"; export default defineComponent({ setup() { const store = useStore(key); const data = computed(() => store.state.name); return { data, }; }, }); </script>

你会发现现在通过useStore拿到的对象,有代码提示了。

但是这样非常麻烦,因为使用useStore是一个非常频繁的事情,所以官方推荐自己手写useStore封装。

创建一个自己的hooks:

typescript
复制代码
import { useStore as baseUseStore } from "vuex"; import { key } from "./store"; export function useStore () { return baseUseStore(key) }

包一层后,再去统一使用这个就不用每次都传key了。

模块化处理

我们创建一个模块,并声明模块的类型为Module,这个类型可以从vuex中导入进来,它接收两个泛型:

typescript
复制代码
Module<S, R>
  • S:表示当前模块自己的state类型声明
  • R:表示根state的类型声明,这个之前就定义过,import引入即可。

store/modules/user.ts

typescript
复制代码
import type { Module } from "vuex"; import { State } from "../index"; const userState = { name: "用户名", }; export type UserState = typeof userState; export default { namespaced: true, state: userState, mutations: { setName(state, payload: string) { state.name = payload; }, }, } as Module<UserState, State>;

这里,mutations这些对象中,函数接收的第一个参数state已经可以通过Module来推断出类型了。

通过typeof,可以不需要手动再写一份类型定义,这种方式适合不存在可选属性的时候。

下面就是注册模块了!

store/index.ts

typescript
复制代码
import { createStore, Store } from "vuex"; import { InjectionKey } from "vue"; import user, { UserState } from "./modules/user"; export type State = { name: string; user?: UserState; }; export const key: InjectionKey<Store<State>> = Symbol(); const store = createStore({ state: { name: "vuex", }, modules: { user, }, }); export default store;

这里就涉及到我一开始说的那个问题了,模块的数据最终是通过state获取,但是数据挂载是通过modules对象,所以我们在定义根的state的类型声明的时候,就不得不使用?:方式,来表示这个模块可以不存在,不然就必须在具体的state数据定义那写上真实数据,这显然不是我们需要的。

使用的时候:

typescript
复制代码
<script lang="ts"> import { defineComponent, computed } from "vue"; import { useStore } from "vuex"; import { key } from "./store"; export default defineComponent({ setup() { const store = useStore(key); const name = computed(() => store.state.user?.name); return { name, }; }, }); </script>

虽然已经有了代码提示,但是在调用的时候你会发现会自动加上可选链?.,因为user可能不存在,所以就导致它里面的属性也是未知的。

所以,有时候我们还不得不这么写:

typescript
复制代码
const name = computed(() => store.state.user!.name);

强制断言数据是存在的。

补充:

我们的state推断已经有了,但是对于getters、commit、dispatch那种理想状态的提示是没有的。

而且现在vuex的代替品:pinia 也已经正式服役了,如果是新项目,可以考虑使用这个,这个对于ts的适配会更加的好。

对于上述vuex的ts推断,官方也是有对应文章的:TypeScript 支持

vue-router的类型推断

vue-router 4.x对于ts的提示是非常友好的,基本上能遇到的都有类型推断,如果你不清楚是什么类型,可以将鼠标放置在对象上,即可查看具体详情。

这里就说一下自定义routes数组类型推断,在之前的文章我提到过通过routes对象来生成对应的侧边栏菜单,但是有的路由并不希望显示在侧边栏,所以一般会增加一个自定义的属性hidden: true这种,但是由于ts类型校验的问题,vue-router的默认类型RouteRecordRaw是没有我们的自定义属性的。导致校验失败。

typescript
复制代码
import { createRouter, createMemoryHistory } from "vue-router"; const router = createRouter({ history: createMemoryHistory(), routes: [ { path: "/", component: import("./views/Home.vue"), hidden: true, }, ], });

我们可以声明一个基于RouteRecordRaw的类型来断言处理。

typescript
复制代码
import { createRouter, createMemoryHistory } from "vue-router"; import type { RouteRecordRaw } from "vue-router"; type MyRouteRecordRaw = RouteRecordRaw & { hidden?: boolean; }; const router = createRouter({ history: createMemoryHistory(), routes: [ { path: "/", component: import("./views/Home.vue"), hidden: true, }, ] as MyRouteRecordRaw[], });

使用这种方式我们还可以针对meta对象进行精细的控制,官方默认的meta类型是一个宽泛的类型声明:

typescript
复制代码
export declare interface RouteMeta extends Record<string | number | symbol, unknown> { }

如果我们想准确控制,也可以通过MyRouteRecordRaw来个性化定制。

例:

typescript
复制代码
import { createRouter, createMemoryHistory } from "vue-router"; import type { RouteRecordRaw } from "vue-router"; type MyRouteRecordRaw = RouteRecordRaw & { hidden?: boolean; meta: { auth: Array<string>; //鉴权 title: string; //页面标题 icon: string; //页面图标 }; }; const router = createRouter({ history: createMemoryHistory(), routes: [ { path: "/", component: import("./views/Home.vue"), hidden: true, meta: { auth: [], title: "首页", icon: "el-icon-menu", }, }, ] as MyRouteRecordRaw[], });

通过这种约束,后续添加的新路由都得有这些配置,这就是ts静态检验的魅力。

axios的类型推断

axios很早就实现了对ts的支持,所以的他的类型推断也是非常完善的,我们这边就提一下对于异步返回值的类型推断。

创建一个axios实例对象:

typescript
复制代码
import axios from "axios"; const http = axios.create(); export default http;

发起请求:

typescript
复制代码
type Response = { userId: number; id: number; title: string; completed: boolean; }; http.get<Response>("https://jsonplaceholder.typicode.com/todos/1").then((res) => { console.log(res); });

当我们去查看res的类型推断是,其实并不是我们想要的。

它抛出的类型其实是axios最原始的数据对象:

javascript
复制代码
{ config: ..., data: ..., headers: ..., request: ..., status: 200, statusText: "" }

其中data才是后端返回的数据,为了方便使用,我们一般会使用拦截器进行一次脱壳处理:

typescript
复制代码
http.interceptors.response.use((response) => { return response.data; });

但是你会发现,我们的返回内容确实已经脱壳了,但是类型推断还是之前的AxiosResponse<Response, any>

解决办法就是我们自己用函数包装一下,控制下return出去的类型推断:

typescript
复制代码
import axios from "axios"; import type { AxiosResponse } from "axios"; const http = axios.create(); export function apiGet<T>(url: string): Promise<T> { return new Promise((resolve, reject) => { http .get(url) .then((res: AxiosResponse<T>) => resolve(res.data)) .catch((err) => reject(err)); }); } export default http;

使用时:

javascript
复制代码
apiGet<Response>("https://jsonplaceholder.typicode.com/todos/1").then((res) => { console.log(res.title); });

此时我们再去查看类型推断,已经变成了Response

此时已经满足了我们的需求,但是在真实的项目中,data的数据他其实是被后端包了一层状态的,一般会包含后端提供的状态码,消息、数据等等,举个例子:

javascript
复制代码
{ result: 1, info: "", data: ... }

所以如果我们想要脱壳拿到真正的数据,其实还得再脱一层,这时,我们需要一些特殊的用法。

typescript
复制代码
import axios from "axios"; import type { AxiosResponse } from "axios"; const http = axios.create(); type BaseResponse = { result: number; info: ""; data: any; }; http.interceptors.response.use((response: AxiosResponse<BaseResponse>) => { return response.data.data; }); export function apiGet<T>(url: string): Promise<T> { return new Promise((resolve, reject) => { http .get(url) .then((res: unknown) => { return resolve(res as T); }) .catch((err) => reject(err)); }); } export default http;

由于axios实例的请求,then得到的参数类型被写死了,一直是一个AxiosResponse;但是实际上我们已经在拦截器里进行脱壳处理,这里拿到的是具体的后端数据,所以我们需要先用unknown覆盖旧的类型推断,然后as重新声明一下抛出的类型。

最终效果使用就符合我们的预期了。

vite的环境变量属性类型推断

webpack的由于还是js类型,所以没法给它做环境变量的推断,目前只有vite可以。

我们创建一个环境变量:.env.development写入以下内容:

bash
复制代码
VITE_BASE_URL="https://www.xxx.com"

此时我们通过import.meta.env.VITE_BASE_URL是没有代码提示和类型推断的。

在src目录下创建一个env.d.ts文件,填入以下内容:

typescript
复制代码
interface ImportMetaEnv { VITE_BASE_URL: string; }

此时我们再回去调用,你会发现有提示和类型推断了!

分类: vue 项目实战 标签: vuevue3TypeScriptts类型推断

评论

全部评论 1

  1. 杰哥
    杰哥
    Google Chrome Windows 10
    Volar厉害啊有点香。但是还是保重身体啊。26岁还3点睡,这怎么顶得住啊

目录