在 VUE3 里如何使用 TS 对类型进行标注?

2,802 阅读1分钟

前言

本文参考 Vue3.0 文档 TypeScript with Composition API 模块。

本文假定用户使用场景是  <script setup> 与 组合式 API ,代码都跑写在 <script setup lang="ts"> 。所以文内大部分代码块里的代码会省略掉 <script setup lang="ts"> 这部分且不考虑选项式 API的情况。

props

props 的类型声明分为以下两种

  1. 运行时声明(自动推导)
  2. 基于类型的声明(泛型)

两种方式编译以后得到的 “运行时的 props ”是一致的(即选项式API 里的 props ),以上两种都可以使用,但不能同时使用两种。

运行时声明(自动推导)

在定义组件内 props 时,defineProps 方法支持从它的参数自动推导类型

const props = defineProps({
  year: { type: Number, required: true },
  month: Number,
  day: Number
})

基于类型的声明(泛型)

注意: 泛型参数可以包含从其他文件引入的类型,但它本身不能是一个直接导入的类型

泛型形式无法定义默认值, 需要开启实验性功能里的 响应式解构 props 才能设定默认值

也可以给 defineProps 传递定义好的对象类型或接口

const props = defineProps<{
  year: number,
  month?: number,
  day?: number
}>()
interface Props {
  year: number,
  month?: number,
  day?: number
}

const props = defineProps<Props>()

// 可以通过 “响应式 props 解构” 定义默认值
// 该功能还属于实验性功能,开启需要做对应配置,具体查看标题引用的:“响应式 props 解构” 文档 
const { year, month = 1, day = 1 } = defineProps<Props>()

emit 

emit 类型定义方式与 props 一致,也是分为 “运行时”、“基于类型”两种方式

运行时声明(自动推导)

const emit = defineEmits(['update']);

基于类型的声明(泛型)

interface Emit {
  /** test emit: update func */
  (e: "update", id: number): void;
}
const emit = defineEmits<Emit>();
emit("update", 1)

ref

ref 类型标注共有三种方式

  1. 自动推导
  2. 类型注解
  3. 泛型

自动推导

import { Ref, ref } from 'vue';

/** 自动推导: 有默认值 */
let year = ref(2022); // Ref<number>

/** 自动推导: 无默认值 */
let yearEmpty = ref(); // Ref<any>

类型注解

import { ref, Ref } from 'vue';

/** 类型注解: 必须有默认值 */
let month: Ref<number | string> = ref('10'); // Ref<string | number>

泛型

import { ref } from 'vue';

/** 泛型: 有默认值 */
let day = ref<number>(10); // Ref<number>

/** 泛型: 无默认值 */
let dayEmpty = ref<number>(); // Ref<number | undefined>

reactive

reactive 类型标注和 ref一样,共有三种方式

  1. 自动推导
  2. 类型注解
  3. 泛型(不推荐使用)

自动推导

import { reactive } from 'vue';

/** 自动推导: 有默认值 */
const tom = reactive({ name: 'Tom' }); // { name: string; }

/** 自动推导: 无默认值 */
const tomEmpty = reactive({}); // {}

类型注解

import { reactive, ref } from 'vue';

interface Dog { name: string }

/** 类型注解: 普通对象 */
const spike: Dog = reactive({ name: 'Spike' }) //  Dog

/** 类型注解: 含 ref 对象 */
const spikeDeepRef: Dog = reactive({ name: ref('Spike') }) // Dog

泛型(不推荐使用)

官方文档: 不推荐使用 reactive() 的泛型参数,因为处理了深层次 ref 解包的返回值与泛型参数的类型不同。

import { reactive, ref } from 'vue';

interface Mouse { name: string }

/** 泛型: 普通对象 */
const mouse = reactive<Mouse>({ name: "jerry" }) // Mouse
=
/** 泛型: 含 ref 对象 */
const mouseDeepRef = reactive<Mouse>({ name: ref('jerry') }) // error:不能将类型“Ref<string>”分配给类型“string

computed

computed 共有三种方式

  1. 自动推导
  2. 泛型
  3. 类型注解( 不推荐使用, 官方文档也未标出 )

自动推导

import { computed, ComputedRef, ref } from 'vue';

const num = ref(10);

/** 自动推导 */
const double = computed(() => num.value * 2) // ComputedRef<number>

泛型

import { computed, ref } from 'vue';

const num = ref(10);

/** 泛型 */
const three = computed<number>(() => num.value * 3) // ComputedRef<number>

类型注解 (不推荐使用)

官方文档: 不推荐使用 reactive() 的泛型参数,因为处理了深层次 ref 解包的返回值与泛型参数的类型不同。

import { computed, ComputedRef, ref } from 'vue';

const num = ref(10);

/** 类型注解*/
const four: ComputedRef<number> = computed(() => num.value * 4) // ComputedRef<number>

const fourError: number = computed(() => num.value * 4) // error: 不能将类型“ComputedRef<number>”分配给类型“number”。

provide

provide 比较特殊,他是无法做到自动推导的,想要让 provide 提供的值被注入时能被准确的标注类型,只能使用 Vue提供的 InjectionKey接口, 它是一个继承自 Symbol 的泛型类型。

未使用 InjectionKey

// 提供者代码
import { provide } from "vue";
let tom = { name: "tom" };
provide("cat", tom); // { name: string; }

// 消费者代码
import { inject } from "vue";
const tom = inject("cat"); // unknown

// 如果明确知道注入进来的类型的话,也可以使用泛型给类型做注解
const tomCorrect = inject<{ name: 'string' }>("cat")  // { name: string; } || undefined

// 注意:此处传入了错误的泛型,但是是不会报错的,产生了一个坑
const tomError = inject<string>("cat") // string | undefine

使用 InjectionKey

// 存储 provide key 的文件代码
import { InjectionKey } from "vue";
export const cat = Symbol() as InjectionKey<{ name: string }>;


// 提供者代码
import { cat } from "./keys.ts"; 
import { provide } from "vue";
let tom = { name: "tom" };
provide(cat, tom); 	// InjectionKey<{ name: string; }>

// 消费者代码
import { inject } from "vue";
import { cat } from "./keys.ts";
const tom = inject(cat); // { name: string; } | undefined

看完代码会发现,以上代码块里,能识别出类型的都会带着 undefined 类型,要去除掉它的话,只需要在注入方传递默认值或类型断言

// 提供者代码
import { provide } from "vue";
let tom = { name: "tom" };
provide("cat", tom); // { name: string; }

// 消费者代码
import { inject } from "vue";

// 类型断言
const tom = inject("cat") as { name: string; }; // { name: string; }

// 默认值
const tomWithDefaultVal = inject<{ name: string }>("cat", { name: "tom" }); // { name: string; }

event

原生的事件对象类型会被标注为 any,需要使用 Event 来显式的标注它的类型。且需要显式地强制转换 event 上的 property

<script setup lang="ts">
function handleChange(event: Event) {
  console.log((event.target as HTMLInputElement).value)
}
</script>

<template>
  <input type="text" @change="handleChange" />
</template>

模板 ref

模板 ref 需要通过一个显式指定的泛型参数和一个初始值 null 来创建

<script setup lang="ts">
import { ref, onMounted } from 'vue'

const el = ref<HTMLInputElement | null>(null)

onMounted(() => {
  el.value?.focus()
})
</script>

<template>
  <input ref="el" />
</template>

组件模板 ref

首先需要通过 typeof 得到其类型,再使用 TypeScript 内置的 InstanceType 工具类型

import MyModal from './MyModal.vue'

const modal = ref<InstanceType<typeof MyModal> | null>(null)

const openModal = () => {
  modal.value?.open()
}
</script>