木灵鱼儿
阅读:6277
正确使用vue3的ts类型声明
前言
使用了ts最头疼的是什么,除了类型声明应该没有第二家了,那么在vue3中如何正确的声明ts类型,代表着我们踏出了认识vue3的第一步,这非常重要,所以为此水个文章,分享给有需要的人。
Volar 插件
一开始我对于Volar并没有太大的需要,因为一直使用的Vetur,而且这个插件刚出来时并不完善,各种视频up讲的那个一键分屏功能其实也并不好用,虽然是个很有意思的东西,但是没有那种非要使用它的点,所以当时的我怀着这么一个疑问?为什么要用Volar ?
现在我就通过两张图告诉你,它有多香!
我们在template里面写代码,绑定变量最烦的是什么,就是我们写了个对象,但是忘了它的属性有哪些啊,使用了Volar配合ts类型声明,就可以再template里面实现代码提示和类型提示,这特么不得吹爆啊,所以还在犹豫什么,用起来。
并且当你的属性或者变量不存在时,template中也会有对应的波浪线错误提示。
data的类型声明
vue3中数据的声明一般使用ref
或者reactive
;这两个方法都是使用的泛型定义的类型,默认情况下他会自动推断出你书写的数据。
<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的数据,此时泛型就无法推断出数据类型了,所以我们需要手动定义。
例:
<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,类型该怎么声明呢?
<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也可以自行推断出来,可以不用自己手动断言类型。
props的类型声明
vue3的props使用其实和vue2是一样的,只不过它对于setup
组件式语法增加了一个新的定义方法defineProps,但其实它的定义格式还是一样的,都可以定义type
、required
等等属性。
我们创建一个test组件,跟以往一样书写一个props属性,需要接收一个string类型的id值来进行显示
<template>
<div>
{{ id }}
</div>
</template>
<script lang="ts">
import { defineComponent } from "vue";
export default defineComponent({
props: {
id: {
type: String,
required: true,
},
},
});
</script>
父级调用:
<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,不能有其它的值,能做到吗?
答案是可以的,我们可以这么写:
<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
<template>
<div>
{{ id }}
</div>
</template>
<script lang="ts" setup>
//定义props
defineProps({
id: {
type: String,
required: true,
},
});
</script>
精准控制:
<script lang="ts" setup>
import type { PropType } from "vue";
//定义props
defineProps({
id: {
type: String as PropType<"1" | "2">,
required: true,
},
});
</script>
computed的类型声明
computed其实可以看成是一个函数,函数的类型咋声明,它就咋声明。
<template>
<div>{{ data }}</div>
</template>
<script lang="ts">
import { defineComponent } from "vue";
export default defineComponent({
computed: {
data(): string {
return "数据";
},
},
});
</script>
除了返回值的类型声明,我们可以声明形参类型。
<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后加上去的。
这也就导致我们在调用模块的属性的时候不得不也加上?.
this.$store.state.模块名?.xxx
甚至我们使用!.
让ts不再报undefined值的错误。
this.$store.state.模块名!.xxx
为此我们也只能是增加调用state的类型提示,以及Mutation、Action的方法中state的自行推断和形参校验,其他就没有了。
getters也能自行推断出state,其他就没了,如果能接受,我们往下看。
this.$store类型推断
首先我先创建一个store目录,创建一个index.ts文件用于存放根state和vuex实例。
store/index.ts
import { createStore } from "vuex";
export type State = {
name: string;
};
const store = createStore({
state: {
name: "vuex",
},
});
export default store;
main.ts挂载store
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
文件,写入以下内容:
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,必须执行以下步骤:
- 定义类型化的
InjectionKey
。 - 将 store 安装到 Vue 应用时提供类型化的
InjectionKey
。 - 将类型化的
InjectionKey
传给useStore
方法。
首先我们创建injection key:
store/index.ts
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作为第二个参数传入:
import { createApp } from "vue";
import App from "./App.vue";
import store, { key } from "./store";
createApp(App).use(store, key).mount("#app");
然后,当我们在使用useStore
的时候,也需要传入key。
<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:
import { useStore as baseUseStore } from "vuex";
import { key } from "./store";
export function useStore () {
return baseUseStore(key)
}
包一层后,再去统一使用这个就不用每次都传key了。
模块化处理
我们创建一个模块,并声明模块的类型为Module
,这个类型可以从vuex中导入进来,它接收两个泛型:
Module<S, R>
- S:表示当前模块自己的state类型声明
- R:表示根state的类型声明,这个之前就定义过,import引入即可。
store/modules/user.ts
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
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数据定义那写上真实数据,这显然不是我们需要的。
使用的时候:
<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可能不存在,所以就导致它里面的属性也是未知的。
所以,有时候我们还不得不这么写:
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
是没有我们的自定义属性的。导致校验失败。
import { createRouter, createMemoryHistory } from "vue-router";
const router = createRouter({
history: createMemoryHistory(),
routes: [
{
path: "/",
component: import("./views/Home.vue"),
hidden: true,
},
],
});
我们可以声明一个基于RouteRecordRaw
的类型来断言处理。
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类型是一个宽泛的类型声明:
export declare interface RouteMeta extends Record<string | number | symbol, unknown> {
}
如果我们想准确控制,也可以通过MyRouteRecordRaw
来个性化定制。
例:
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实例对象:
import axios from "axios";
const http = axios.create();
export default http;
发起请求:
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最原始的数据对象:
{
config: ...,
data: ...,
headers: ...,
request: ...,
status: 200,
statusText: ""
}
其中data才是后端返回的数据,为了方便使用,我们一般会使用拦截器进行一次脱壳处理:
http.interceptors.response.use((response) => {
return response.data;
});
但是你会发现,我们的返回内容确实已经脱壳了,但是类型推断还是之前的AxiosResponse<Response, any>
解决办法就是我们自己用函数包装一下,控制下return出去的类型推断:
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;
使用时:
apiGet<Response>("https://jsonplaceholder.typicode.com/todos/1").then((res) => {
console.log(res.title);
});
此时我们再去查看类型推断,已经变成了Response
!
此时已经满足了我们的需求,但是在真实的项目中,data的数据他其实是被后端包了一层状态的,一般会包含后端提供的状态码,消息、数据等等,举个例子:
{
result: 1,
info: "",
data: ...
}
所以如果我们想要脱壳拿到真正的数据,其实还得再脱一层,这时,我们需要一些特殊的用法。
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
写入以下内容:
VITE_BASE_URL="https://www.xxx.com"
此时我们通过import.meta.env.VITE_BASE_URL
是没有代码提示和类型推断的。
在src目录下创建一个env.d.ts
文件,填入以下内容:
interface ImportMetaEnv {
VITE_BASE_URL: string;
}
此时我们再回去调用,你会发现有提示和类型推断了!
版权申明
本文系作者 @木灵鱼儿 原创发布在木灵鱼儿 - 有梦就能远航站点。未经许可,禁止转载。

相关推荐
利用发布订阅模式封装一个IntersectionObserver
前言用于判断当前元素是否出现在视口区域,以此来实现懒加载已经是日常业务必须的东西了,但是如何去判断这个元素是否出现在视口中,是一个非常头疼的地方。很久以前,我们通过dom.offsetTop的方式,不断的累加自身和父级元素的offsetTop值,得到元素具体文档顶部的距离,然后判断这个距离是否小于等于当前视窗的高度+滚动条scrollTop,如果是的话,说明用户已经滚动到了,或者滚过去了,这个时候就得触发图片懒加载。但是这种方式十分痛苦,需要递归计算offsetTop的值,而且性能不是很好。后来浏览器又提供了getBoundClientRect的方法,这个方法会返回当前元素距离视窗的四个...
ts封装一个localStorage和sessionStorage的类
前言希望存储一个对象的时候能自动JSON转成字符串,存储一个string类型的值时,不需要再JSON化了,不然又多两引号,和原生存储使用会有出入,我的宗旨就是能和原生效果保持一致的情况下增加一些方便的处理。拿取的时候就会有一些顾虑了,因为拿到的值就是字符串类型,所以我加了一个额外的参数来判断是否需要JSON解析,默认是需要的,这个配置用于这个值我可能自己手动转成字符串存的,它的源值是一个对象,我不希望取值的时候被解析出来,我就要它原样给我,应对这种情况加了一个配置判定。在拿取的时候返回的值类型是any,显然这不是我想要的,我希望能准确判定这个类型,于是通过泛型的方式进行约束。另一个考量是...
利用typescript实现原生css变量的主题切换功能类
前言主题的切换研究过好久了,个人认为目前最好的两种实现:原生css变量实现,性能好,但是不太兼容旧设备利用预处理scss这种做一个主题类名,通过最上层类名变化从而改变嵌套的子类名的颜色,兼容性好,但是需要把所有的类都抽出来单独设置。本着简单+性能的目标,我采用了第一种方案,这种方案也是大佬张鑫旭提供了,文章是:《link rel=alternate网站换肤功能最佳实现》有兴趣弄的话可以先看看大佬的文章了解原理。封装主题的链接引入采用如下格式:<!-- theme --> <link href="<?php $this->options->th...
实现一个点击空白区域关闭显示的自定义指令
typescriptimport type { DirectiveBinding } from "vue"; export default { bind(el: HTMLElement, binding: DirectiveBinding<Function>) { //声明一个给document绑定的事件 function documentClick(event: Event) { const target = event.target as unknown as Node; if (el.contains(t...
使用vue2.7的一些踩坑事项
eslint校验的一些问题(暂时无解)在初始化项目时勾选了eslint校验之后,升级vue 2.7版本后,eslint-plugin-vue这个插件需要升级到9+版本,我目前使用的版本是:"eslint-plugin-vue": "^9.4.0"具体的一些可以参考官方提供的2.7升级指南:2.7日志官方居然把这个写在了变更日志里面,按道理最好官方的文档上也有一份说明的,但是目前没有。虽然eslint的依赖更新到新版后确实解决了一些项目启动报错的问题,但是有时候我们的SFC单文件组件开发的时候,template中的一些变量绑定还是会出现波浪线警告,这...
vue-router4 新增功能
动态路由这个其实在3版也有,不过3版原来使用的api是addRoutes,这个api顾名思义,就是可以传入一个路由数组来进行注册,后来被废弃,现在3版和4版统一采用addRouteapi。addRoute一次只能添加一个路由,但是有两种用法:添加一条新路由router.addRoute({ path: "/about", name: "About", component: () => import("../views/About.vue"), })给已存在的路由注册一个子路由,父路由必须有name属性route...
vue-router4 一些常见配置的改动
创建路由实例方式改变3版本的时候是通过new的方式创建路由实例,4版本是改用了createRouter的方式:import { createRouter } from 'vue-router' const router = createRouter({ // ... })路由模式配置改变3版本路由模式是由mode属性控制,值为字符串,现在通过import引入不同函数来创建不同的路由模式:"history"改为createWebHistory()"hash"改为createWebHashHistory()"abstract"...
vue3 filter过滤器
vue3直接移除了filter过滤器,官方推荐,如果要使用过滤器可以使用computed计算属性和method函数来代替。如果使用了全局过滤器,官方也提供了一个属性来进行迁移修复,但是也只推荐用于迁移。const app = createApp(App) //给当前app的全局属性上挂载一个过滤器对象 app.config.globalProperties.$filters = { currencyUSD(value) { return '$' + value } }使用的时候:<template> <h1>Bank Account Bala...
vue3 keyCode修饰符
vue2我们在监听按键事件的时候,是可以通过在事件后面加上按键码来实现监听某一个按键的。<input @keyup.13="submit" />甚至我们还可以使用按钮别名alias的方式调用<input @keyup.enter="submit" />还可以全局自定义别名,通过Vue.config.keyCodes属性。Vue.config.keyCodes = { f1: 112 }随着ECMAScript的标准推荐,官方已经不推荐使用keyCode键码了,这个功能在js的官方定义上是一个已弃用的状态,最新的标准是使用...
vue3 取消$on、$off、$once
在vue2的时候,我们可以通过$on、$off、$once、$emit来实现一个事件总线EventBus,但是vue3的时候,又换了一套逻辑思想。官方不鼓励使用EventBus在组件之间进行通信,虽然短期内确实是非常简单的解决方案,但是从长远来看,它总是很难维护的,所以官方更加推荐使用其他方式:父子组件之间可以通过Props和自定义事件来进行沟通,如果是兄弟组件,可以通过父级组件来沟通使用Provide / inject方式来跨层级沟通,比如与插槽的内容组件。Provide / inject也可以用于跨层级沟通,以避免props,emit事件层级过深的问题通过使用插槽的形式来避免层级沟通...
杰哥
Google Chrome Windows 10Volar厉害啊有点香。但是还是保重身体啊。26岁还3点睡,这怎么顶得住啊