前言
- 我也是
Vue3 + TS
刚起步,还有很多不了解的东西,写文章主要是为了记录一下自己的学习历程还有就是记录自己遇到的问题,方便以后的查阅; - ❗ 注意 :
- 在
Vue3
中,ref、reactive
本身是泛型函数; - 关于这块,大家也可以去看看官网上的:TypeScript 与 组合式API;
- 在
一、为 ref() 标注类型
-
场景和好处:
- 为
ref()
标注类型之后,既可以在 给ref对象的value
属性 赋值时 校验 数据类型 ,同时在 使用value
的时候可以 获得 代码提示;
- 为
-
本质 :
- 是给
ref
对象的value
属性 添加 类型约束;
- 是给
-
🎯 标注类型 :
ref()
和TS
的配合通常分为 两种情况,取决于想为ref内的值定为 简单类型 的数据还是 复杂类型 的数据;- ⛳ 简单数据类型 :
- 直接使用 类型推导(照常写,不用标注类型);
- 🎀 复杂数据类型 :
-
1️⃣ 可以导入 Ref 这个类型;
-
语法 :
html<script> import type { Ref } from 'vue'; const / let 变量名: Ref<类型> = ref(初始值); </script>
-
-
2️⃣ 在调用 ref() 时传入一个 泛型参数 ,类覆盖默认的推导行为(类型推断);
-
语法 :
html<script> import { ref } from 'vue'; const / let 变量名 = ref<类型>(初始值); </script>
-
-
-
❗ 注意 :
- 如果你 指定了一个泛型参数 但 没有给出初始值 ,那么最后得到的将是一个包含
undefined
的 联合类型;
- 如果你 指定了一个泛型参数 但 没有给出初始值 ,那么最后得到的将是一个包含
-
代码展示:
html<script setup lang="ts"> import { ref } from 'vue'; import type { Ref } from 'vue'; // 简单数据类型 - 直接使用类型推断 let isLoading = ref(true); // 复杂数据类型 - 通过泛型制定类型(既可以使用 类型别名 - type 也可以使用 接口 - interface) // 接口 interface GoodsItem { id: number; goods_name: string; price: number; img_url: string; } // 类型别名 type GoodsItem = { id: number; goods_name: string; price: number; img_url: string; } // ✅ const goodsInfo = ref<GoodsItem[]>([ { id: 1, goods_name: '香蕉', price: 12, img_url: '' } ]); // ❌ const arr1: Ref<GoodsItem[]> = ref([ { id: 1, goods_name: '香蕉', price: 12, img_url: '' } ]);
二、为 reactive() 标注类型
- 场景和好处:
- 为
reactive()
标注类型之后,既可以在 响应式对象在修改属性值 的时候 约束类型 ,也可以在 使用时 获得 代码提示;
- 为
- 🎯 标注类型 :
- ⛳ 类型推导 :
reactive()
也会隐式地从它的参数中推导类型;
- 🎀 显示标注 :
- 可以使用 接口
(interface)
或 类型别名(type)
; - 见代码展示;
- 可以使用 接口
- ⛳ 类型推导 :
- ❗ 注意 :
- 不推荐使用
reactive()
的 泛型参数 ,因为 处理了深层次ref
解包的返回值 与 泛型参数的类型不一同;
- 不推荐使用
- 代码展示:
html
<script setup lang="ts">
// EXPLAIN 为 reactive() 标注类型
import { reactive } from 'vue';
// ❌ 方式一:类型推导
const obj = reactive({
a: 1,
b: 2
});
// ✅ 方式二:显示标注
// interface - 接口
interface UserInfo {
username: string;
age: number;
likes?: string;
}
// type - 类型别名
type UserInfo = {
username: string;
age: number;
likes?: string;
}
const userInfo: UserInfo = reactive({
username: '禁止摆烂_才浅',
age: 22
});
</script>
三、为 computed() 标注类型
-
说明 :
- 计算属性 通常是由 已知的响应式数据得到 ,所以 依赖的数据类型一旦确定 通过 自动推导 就可以知道 计算属性的类型,另外根据最佳实践,计算属性多数情况下是只读的,不做修改,所以配合TS一般只做代码提示;
- 如果想要修改,可通过 泛型参数 指定类型;
-
🎯 标注类型 :
- ⛳ 类型推导 :
computed()
会自动从其计算函数的返回值上推导出类型;
- 🎀 显示标注 :
- 通过 泛型参数 显示指定类型;
- 见代码展示;
- ⛳ 类型推导 :
-
❗ 注意 :
- 给计算属性指定类型之后,赋的值 和 返回值 的 数据类型 都 必须满足所指定的类型;
-
代码展示:
html<script setup lang="ts"> import { ref, reactive, computed } from 'vue'; type UserInfo = { username: string; age: number; likes?: string; } const userInfo: UserInfo = reactive({ username: '禁止摆烂_才浅', age: 22 }); // EXPLAIN 为 computed() 标注类型 // 只读 const str = computed(() => userInfo.age); // 如果返回值不是 number 类型的就会报错 const str = computed<number>(() => userInfo.age); // 修改 let likes = ref<string>(''); // 通过 泛型参数 指定要赋的值的类型,不符合类型就会报错(赋的值 和 返回值 的 类型 都是 string) const str1 = computed<string>({ set(val) { console.log(val); }, get: () => userInfo.username + userInfo.age }); </script> <template> <header>{ str + str1 }}</header> <input type="text" v-model="likes"> <button @click="str1 = likes">改变计算属性的值</button> </template>
-
错误展示:
四、为 组件的 props 标注类型
4.1、为什么给 props
标注类型?
- 确保给组件传递的
prop
是类型安全的;- 比如:子组件要的是
number
类型的数据,你传递的是string
类型的数据;
- 比如:子组件要的是
- 在组件内部使用
props
和 为组件传递prop属性的时候会有良好的代码提示;
4.2、props类型标注
-
语法 :
- ❌ 运行时声明 - 类型推导;
- 当使用
<script setup>
时,defineProps()
宏函数支持从它的参数中推导类型;
- 当使用
- ✅ 基于类型的声明 - 通过
defineProps()
宏函数 对 组件props 进行 类型标注;
- ❌ 运行时声明 - 类型推导;
-
❗ 注意 :
- 两种方式可以任选一种进行标注,但是不能同时出现;
-
需求:按钮组件有两个参数,color类型为string且为必传项,size类型为string且为可选项,怎么定义类型?
ts// ✅ 基于类型的声明 // 按钮组件传递prop属性的时候,必须满足color是必传且为string,size可选项,类型为string // 1、使用 类型别名 或 接口 定义 Props类型 interface Props { color: string; size?: string; } // 2、使用 defineProps 注解类型 const props = defineProps<Props>();
html<!-- ❌ 运行时声明 --> <script setup lang="ts"> const props = defineProps({ foo: { type: String, required: true }, bar: Number }); props.foo // string props.bar // number | undefined </script>
4.3、props默认值设置
-
场景:
Props
中的 可选参数 通常除了 指定类型 之外还需 提供默认值 ,可以使用withDefaults()
宏函数 进行设置; -
语法 :
ts// withDefaults() 可以直接使用 const props = withDefaults(defineProps<类型>(), { 可选项属性名: 对应类型的默认值 });
- 说明:
- 如果用户传递了数据,就是用传递的;否则就是用默认值。
- 说明:
-
代码展示:
- 需求:按钮组件的
size
属性的默认值为middle
;
ts<scropt setup lang="ts"> type Props = { color: string; size?: string; } const props = widthDefaults(defineProps<Props>(), { size: 'middle' }); </script>
- 需求:按钮组件的
-
小练习:给按钮组件添加一个
btnType
属性,类型为success
、danger
、warning
三选一,默认值为success
,就基于上面的例子写;-
参考代码:
ts<scropt setup lang="ts"> // 使用 类型别名 type Props = { color: string; size?: string; // 对于 btnType,需求中已经指定了为 字面量类型,所以即使默认值为 success,也要在类型注解中标明,如果是string类型,就可以不用写 btnType?: 'success' | 'danger' | 'warning'; } // 使用 接口 interface Props { color: string; size?: string; // 对于 btnType,需求中已经指定了为 字面量类型,所以即使默认值为 success,也要在类型注解中标明,如果是string类型,就可以不用写 btnType?: 'success' | 'danger' | 'warning'; } // 给 props 标注类型 并 设置默认值 const props = withDefaults(defineProps<Props>(), { size: 'middle', btnType: 'success' }); </script>
-
五、为 组件的 emits 标注类型
-
作用 :
- 可以 约束事件名称 并给出自动提示,确保不会拼写错误,同时 约束传参类型 ,不会发生参数类型错误;
-
步骤 :
- 1️⃣ 通过 接口
(interface)
或 类型别名(type) 定义Emite
类型; - 2️⃣ 通过
defineEmits<Emits>()
泛型传参; - 3️⃣ 最后就可以正常使用了
emits('事件名', 参数)
;
- 1️⃣ 通过 接口
-
语法 :
ts// 定义事件类型 Emits【类型别名(type) 和 接口(interface) 二选一即可】 // 类型别名 - type type Emits = { // 如果函数没有返回值,返回的类型就是 void,否则,返回到类型就是具体的某种类型 (e: '事件名', 函数参数1: 类型, 函数参数2: 类型, ...): void; } // 接口 - interface interface Emits { (e: '事件名', 函数参数1: 类型, 函数参数2: 类型, ...): void; } // 给泛型传参 const emits = defineEmiots<Emits>();
-
代码展示:
-
目标文件:
App.vue
;html<script setup lang="ts"> import Son from '@/views/Son.vue'; const receiveMsg = (msg: string) => { console.log(msg); } </script> <template> <Son @get-msg="receiveMsg" /> </template>
-
目标文件:
views/Son.vue
;html<script setup lang="ts"> // 接口 interface Emits { (e: 'get-msg', msg: string): void; } // 类型别名 type Emits = { (e: 'get-msg', msg: string): void; } const emits = defineEmits<Emits>(); // 按照 JS 的写法 /* const emits = defineEmits({ // 不需要对参数进行验证 getMsg: null, // 需要对参数进行验证 getMsg: (val) => { // 具体的验证逻辑 } }); */ const handler = () => { emits('get-msg', '禁止摆烂_才浅'); } </script> <template> <button @click="handler">给父组件传递数据</button> </template>
-
-
小练习:
- 基于上面的代码,Son组件再触发一个事件
get-list
,传递参数类型下图所示: - 代码参考:
-
目标文件:
App.vue
html<script setup lang="ts"> import Son from '@/views/Son.vue'; interface GoodsItem { id: number; name: string; } const receiveGoods = (list: GoodsItem[]) => { console.log(list); } </script> <template> <Son @get-list="receiveGoods" /> </template>
-
目标文件:
src/views/Son.vue
html<script setup lang="ts"> // 第一步:定义 Emits 类型 type GoodsItem = { id: number; name: string; } interface Emits { (e: 'get-list', list: GoodsItem[]): void; } // 第二步:给 defineEmits传递泛型参数 const emits = defineEmits<Emits>(); // 第三步:使用 const sendGoods = () => { emits('get-list', [{ id: 1001, name: '冬季棉袜' }]); } </script> <template> <button @click="sendGoods">给父组件传递数据</button> </template>
-
- 基于上面的代码,Son组件再触发一个事件
六、为 模板引用(ref属性) 标注类型
-
给模板引用标注类型,本质 上是给
ref
对象 的value
属性 添加了 类型约束 ,约定value
属性 中存放的是 特定类型 的 DOM对象,从而在使用的时候获得响应的代码提示; -
模板引用需要通过一个 显式指定 的 泛型参数 和 一个初始值
null
来创建;-
简单来讲:通过 具体的DOM类型 【联合】 null 作为 泛型参数;
-
语法 :
tsconst ref变量 = ref<具体的DOM类型 | null>(null);
-
-
❗ 注意 :
- 为了严格的类型安全,有必要在访问
el.value
时使用 可选链【ES6中的链判断】 或 类型守卫 :- 因为 直到组件被挂载前 ,这个
ref
的值都是初始的null
,并且由于v-if
的行为将引用的元素卸载时也可以设置为null
;
- 因为 直到组件被挂载前 ,这个
- 也就是说给
ref
标注类型的时候,必须使用 联合类型 ,ref<具体的元素类型 | null>();
- 为了严格的类型安全,有必要在访问
-
代码展示:
html
<script setup lang="ts">
import { ref } from 'vue';
const btn_ref = ref<HTMLButtonElement | null>(null);
</sctipt>
<template>
<button ref="btn_ref">为模板引用标注类型</button>
</template>
七、为 组件模板引用 标注类型
-
有时,你可能需要为一个子组件添加一个模板引用,以便调用它公开的方法。举例来说,我们有一个
MyModal
子组件,它有一个打开模态框的方法:html<!-- MyModal.vue --> <script setup lang="ts"> import { ref } from 'vue' const isContentShown = ref(false) const open = () => (isContentShown.value = true) defineExpose({ open }) </script>
-
为了获取
MyModal
的类型,我们首先需要通过typeof
得到其类型,再使用 TypeScript 内置的InstanceType
工具类型来获取其实例类型:html<!-- App.vue --> <script setup lang="ts"> import MyModal from './MyModal.vue' const modal = ref<InstanceType<typeof MyModal> | null>(null) const openModal = () => { modal.value?.open() } </script>
-
注意 :
- 如果你想在 TypeScript 文件而不是在 Vue SFC 中使用这种技巧,需要开启 Volar 的 Takeover 模式。
-
如果组件的具体类型无法获得,或者你并不关心组件的具体类型,那么可以使用
ComponentPublicInstance
。这只会包含所有组件都共享的属性,比如$el
。tsimport { ref } from 'vue' import type { ComponentPublicInstance } from 'vue' const child = ref<ComponentPublicInstance | null>(null)
八、为 事件处理函数 标注类型
-
为什么事件处理函数需要标注类型?
-
原生DOM事件处理函数 的 参数默认 会 自动标注 为 any类型,没有任何提示,为了获得良好的类型提示,需要手动标注类型;
-
没有类型标注时,函数的参数会隐式地标注为
any
类型。这也会在tsconfig.json
中配置了"strict": true
或"noImplicitAny": true
时报出一个 TS 错误。因此,建议显示地为事件处理函数的参数标注类型。此外,在访问event
上的属性时,可能需要 类型断言 :tsfunction handlerChange(event: Event) { console.log((event.target as HTMLInputElement).value); }
-
-
🎯 为事件处理函数标注类型:
- 事件处理函数的类型标注主要做两个事;
- 1️⃣ 给 事件对象 形参
e
标注为Event
类型,可以获得事件对象的相关类型提示; - 2️⃣ 如果需要 更加精确的DOM 类型提示 可以使用 类型断言
(as)
进行操作;
-
小拓展:
- 当我们绑定一个事件之后,经常会使用到
e.target
,但是我们又不知道e.target
的类型,这时候该怎么去进行断言呢? - 我们可以使用
console.dir(e.target)
在控制台打印,打印得到的是一个对象,翻到对象的最后面,就可以看到这样的字段[[prototype]]: xxx
,这个xxx
就是我们需要的类型,一般这个类型都是这样的HTML元素名称Element
,也不完全一样哈,具体的还是要看打印出来的是啥;
- 当我们绑定一个事件之后,经常会使用到
-
我大概总结了以下常见的类型,请看下表;
标签名称 类型 div
HTMLDivElement
img
HTMLImageElement
span
HTMLSpanElement
input
(不用管type)HTMLInputElement
button
HTMLButtonElement
a
HTMLAnchorElement
p
HTMLParagraphElement
ul
HTMLUListElement
li
HTMLLIElement
h1 ~ h6
HTMLHeadingElement
textarea
HTMLTextAreaElement
table
HTMLTableElement
tr
HTMLTableRowElement
th
HTMLTableCellElement
td
HTMLTableCellElement
ol
HTMLOListElement
-
代码展示:
html<script setup lang='ts'> import { ref } from 'vue'; const text = ref<string>(''); const handlerChange = (e: Event) => { console.log((e.target as HTMLInputElement).value); } const addType = (e: Event) => { console.dir(e.target); } </script> <template> <div> <input type="text" v-model="text" @change="handlerChange"> <br> <button @click="addType">为事件处理函数添加类型注解</button> </div> </template> <style scoped lang='scss'> </style>
九、为 provide / inject 标注类型
-
这块我还是不太明白,等搞明白了,把这一章节,再优化一下哈;
-
provide
和inject
通常会在不同的组件中运行。要正确地为 注入的值 标记类型 ,Vue
提供了一个InjectionKey
接口 ,它是一个继承自Symbol
的泛型类型,可以用来在提供者和消费者之间同步注入值的类型;tsimport { provide, inject } from 'vue'; import type { InjectionKey } from 'vue'; const key = Symbol() as InjectionKey<string>; // 若提供的是非string类型的值,就会报错 provide(key, 'foo'); // foo 的类型: string | undefined const foo = inject(key);
-
建议将注入
key
的类型 放在一个单独的文件中,这样它就可以被多个文件导入; -
当使用字符串注入
key
时,注入的值的类型是unknown
,需要通过泛型参数显示声明:ts// 类型: string | undefined const foo = inject<string>('foo');
-
注意 :
- 注入的值仍然可能是
undefined
,因为无法保证提供者一定会在运行时provide
这个值。
- 注入的值仍然可能是
-
当提供了一个默认值后,这个
undefined
类型就可以移除了:ts// 类型: string const foo = inject<string>('foo', 'bar');
-
如果你确定该值将始终被提供,则,还可以 强制转换 该值:
tsconst foo = inject('foo') as string;
十、类型声明文件
- 概念:
- 在
TS
中以d.ts
为后缀的文件就是类型声明文件,主要 作用 是为 js模块 提供 类型信息支持,从而获得类型提示;
- 在
10.1、d.ts
的来源以及是怎么生效的?
d.ts
是如何生效的?- 在使用
js
某些模块的时候,TS 会 自动导入 模块对应 的d.ts
文件,以提供类型提示;
- 在使用
d.ts
是如何来的?- 库本身是使用
TS
编写的,在打包的时候经过 配置 自动生成 对应的d.ts
文件 (axios
本身就是TS
编写的); - 有些库本身并不是使用
TS
编写的,无法直接生成配套的d.ts
文件,但是也想获得类型提示,此时就需要DefinitelyTyped
提供类型声明文件;DefinitelyTyped
是一个TS类型定义的仓库,专门为JS编写的库可以提供类型声明;
- 库本身是使用
10.2、实际应用场景介绍
- 比如我们有些项目可能会使用到
jQuery
,但是jQ
它是用JS
编写的,没有类型声明文件,此时,我们就可以安装@types/jquery
为jQuery
提供类型声明; - 如果我们没有安装
@types/jquery
,导入jQuery
就会报错,报错信息如下图所示: - 安装好这个插件之后,就不会报错了;
- 如果在导入包的时候,报 无法找到xxx模块的声明文件 ,我们就需要下载对应的包了,
@types/没有声明文件的包名
(这个格式基本是固定的); - 也可以去DefinitelyTyped的官网进行搜索,看具体是啥;
10.3、TS内置类型声明文件
- TS为JS运行时可用的所有标准化内置API都提供了类型声明文件,这些文件既不需要编译生成,也不需要第三方提供;
10.4、自定义类型声明文件
d.ts
文件在项目中是可以进行自定义创建的,通常有两种作用:- ❗❗ ✅ 第一种:共享TS类型(重要);
- ❌ 第二种:给JS文件提供类型(了解);
- ❗ 注意 :
- 共享TS类型 :
- 为了区分普通模块的导入,可以加上
type
关键词;import type { 要导入的类型 } from '类型文件的路径';
- 注意区分是哪种导出方式,再决定使用哪种导入方式,一般都是按需;
- 为了区分普通模块的导入,可以加上
- 给JS文件提供类型 :
- 通过
declare
关键词 可以为 js文件 中的变量声明对应类型,这样js导出的模块在使用的时候也会获得类型提示; .js
文件 和.d.ts
文件名必须一致;
- 通过
- 共享TS类型 :
- 场景:
- 共享TS类型:
- 给JS文件提供类型:
10.5、.ts
和 d.ts
文件对比
TS
中有两种文件类型,一种是.ts
,一种是.d.ts
;.ts
:- 既可以包含类型信息也可以写逻辑代码;
- 可以被编译为
js
文件;
.d.ts
:- 只能 包含 类型信息,不可以写逻辑代码;
- 不会 编译 为
js
文件,仅做类型校验检查; - 使用场景:
- 业务中共享类型;
十一、拓展:对象的非空值处理
11.1、空值概念
- 当 对象的属性 可能是
null
或undefined
的时候,称之为 "空值" ,尝试 访问空值身上的属性或方法 会发生 类型错误; - 简单来说就是我们在
JS
中经常遇到的 "不能从 undefined 上面读取 xxx属性 或 function";
11.2、空值的解决方案
11.2.1、可选链(ES6的链判断) - ?.
-
可选链
?.
是一种访问 嵌套对象属性 的安全方式,可选链前面的值 为null
或undefined
时,它会 停止运算; -
使用方式:
- 在可能为空值的前面加上
?.
即可;
- 在可能为空值的前面加上
-
代码展示:
ts<script setup lang="ts"> import { ref, onMounted } from 'vue'; const ipt_ref = ref<HTMLButtonElement | null>(null); onMounted(() => { // 确保 前面的 ipt_ref.value 不为空值的时候,再往下运;如果为空值,就直接停掉了 ipt_ref.vlaue?.focus(); }); </script>
11.2.2、常规的逻辑判断
- 通过逻辑判断,确保 有值 的时候 才继续执行后面的属性访问语句;
- 常用的逻辑判断有以下几种方式:
if 语句
;! - 取反
;&& - 逻辑与 / || - 逻辑或
;
11.2.3、非空断言 - !
(TS专属)
-
❗❗ 了解就行,开发中最好别用(缺点太明显了),知道有这么一回事就行了;
-
非空断言(
!
) 是指我们明确谁知道当前的值一定不是null
或undefined
,让 TS 通过类型校验; -
❗❗ 注意:
- 使用飞控断言要格外小心,它没有实际的JS判断逻辑 ,只是通过了TS的类型校验,容易直接把空值出现在实际的执行环境中;
-
代码展示:
ts<script setup lang="ts"> import { ref, onMounted } from 'vue'; const ipt_ref = ref<HTMLButtonElement | null>(null); onMounted(() => { // 我明确知道当前的值不为 null / undefined,让 TS 通过类型校验 ipt_ref.vlaue!.focus(); }); </script>