前端开发小技巧 -【Vue3 + TS】 - 为【ref()、reactive()、computed()、事件处理函数、ref、props、emits】标注类型

前言

  • 我也是 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属性,类型为successdangerwarning三选一,默认值为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('事件名', 参数)
  • 语法

    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>

六、为 模板引用(ref属性) 标注类型

  • 给模板引用标注类型,本质 上是给 ref对象value属性 添加了 类型约束 ,约定 value属性 中存放的是 特定类型DOM对象,从而在使用的时候获得响应的代码提示;

  • 模板引用需要通过一个 显式指定泛型参数一个初始值 null 来创建;

    • 简单来讲:通过 具体的DOM类型 【联合】 null 作为 泛型参数

    • 语法

      ts 复制代码
      const 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

    ts 复制代码
    import { 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 上的属性时,可能需要 类型断言

      ts 复制代码
      function 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 标注类型

  • 这块我还是不太明白,等搞明白了,把这一章节,再优化一下哈;

  • provideinject 通常会在不同的组件中运行。要正确地为 注入的值 标记类型Vue 提供了一个 InjectionKey接口 ,它是一个继承自 Symbol的泛型类型,可以用来在提供者和消费者之间同步注入值的类型;

    ts 复制代码
    import { 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');
  • 如果你确定该值将始终被提供,则,还可以 强制转换 该值:

    ts 复制代码
    const 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/jqueryjQuery 提供类型声明;
  • 如果我们没有安装@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类型:
    • 给JS文件提供类型:

10.5、.tsd.ts 文件对比

  • TS中有两种文件类型,一种是.ts,一种是.d.ts
  • .ts
    • 既可以包含类型信息也可以写逻辑代码;
    • 可以被编译为js文件;
  • .d.ts
    • 只能 包含 类型信息,不可以写逻辑代码;
    • 不会 编译 为 js文件,仅做类型校验检查;
    • 使用场景:
      • 业务中共享类型;

十一、拓展:对象的非空值处理

11.1、空值概念

  • 对象的属性 可能是 nullundefined 的时候,称之为 "空值" ,尝试 访问空值身上的属性或方法 会发生 类型错误
  • 简单来说就是我们在 JS 中经常遇到的 "不能从 undefined 上面读取 xxx属性 或 function"

11.2、空值的解决方案

11.2.1、可选链(ES6的链判断) - ?.

  • 可选链 ?. 是一种访问 嵌套对象属性 的安全方式,可选链前面的值nullundefined 时,它会 停止运算

  • 使用方式:

    • 在可能为空值的前面加上 ?. 即可;
  • 代码展示:

    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专属)

  • ❗❗ 了解就行,开发中最好别用(缺点太明显了),知道有这么一回事就行了

  • 非空断言(!) 是指我们明确谁知道当前的值一定不是 nullundefined,让 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>
相关推荐
Boilermaker199210 分钟前
【Java EE】SpringIoC
前端·数据库·spring
中微子21 分钟前
JavaScript 防抖与节流:从原理到实践的完整指南
前端·javascript
天天向上102436 分钟前
Vue 配置打包后可编辑的变量
前端·javascript·vue.js
芬兰y1 小时前
VUE 带有搜索功能的穿梭框(简单demo)
前端·javascript·vue.js
好果不榨汁1 小时前
qiankun 路由选择不同模式如何书写不同的配置
前端·vue.js
小蜜蜂dry1 小时前
Fetch 笔记
前端·javascript
拾光拾趣录1 小时前
列表分页中的快速翻页竞态问题
前端·javascript
小old弟1 小时前
vue3,你看setup设计详解,也是个人才
前端
Lefan1 小时前
一文了解什么是Dart
前端·flutter·dart
Patrick_Wilson1 小时前
青苔漫染待客迟
前端·设计模式·架构