Vue 3源码极客之:`Vue`的`TypeScript`:如何为`props`和`emits`进行类型定义。

大家好!欢迎来到今天的Vue 3源码极客小课堂。我是你们的老朋友,今天咱们要聊聊Vue 3中 TypeScript 的那些事儿,重点是怎样给 propsemits 安排明白的类型定义。说白了,就是怎么让我们的组件既能高效工作,又能让 TypeScript 安心,不再报错。

先别紧张,虽然听起来有点“极客”,但实际上没那么难。咱们争取用最接地气的方式,把这事儿说明白。

开胃小菜:为什么要类型定义?

在深入 propsemits 之前,先简单聊聊为什么要用 TypeScript。如果你已经对 TypeScript 的好处了如指掌,可以跳过这部分。

想象一下,你写了一个组件,需要接收一个 name 属性,然后兴高采烈地用了它。结果呢?

  • 某一天,你或者你的同事手一抖,把 name 传成了数字 123
  • 或者,你以为组件会 emit 一个 update 事件,结果吭哧吭哧写了一堆代码,最后发现组件根本没这个事件。

这种时候,如果没有类型检查,你可能要等到运行时才会发现问题,到时候 debug 起来,那叫一个酸爽。

TypeScript 的出现,就是为了避免这种尴尬。它就像一个尽职尽责的门卫,在你写代码的时候,就帮你检查类型是否匹配,提前发现潜在的错误。

props 的类型定义:三种姿势,各有千秋

在 Vue 3 中,定义 props 的类型,主要有三种方式:

  1. 运行时声明 (Runtime Props Declaration)
  2. 基于类型的声明 (Type-Based Props Declaration)
  3. 混合方式 (Hybrid Approach)

咱们一个个来看。

1. 运行时声明 (Runtime Props Declaration)

这是 Vue 2 中最常见的写法,在 Vue 3 中依然可用。它的核心思想是,在 props 选项中,使用对象字面量来声明属性,并指定它们的类型。

import { defineComponent } from 'vue';

export default defineComponent({
  props: {
    name: {
      type: String,
      required: true,
    },
    age: {
      type: Number,
      default: 18,
    },
    isActive: {
      type: Boolean,
      default: false,
    },
    //允许传入多种类型
    info:{
        type:[String,Number]
    }
  },
  setup(props) {
    console.log(props.name); // 类型推断为 string
    console.log(props.age);  // 类型推断为 number
    console.log(props.isActive); // 类型推断为 boolean

    // props.name = 'new name'; // 错误!props 是只读的

    return {};
  },
});

这种方式的优点是简单直观,易于理解。缺点是,类型信息是在运行时声明的,TypeScript 只能根据 type 属性进行有限的类型推断。

2. 基于类型的声明 (Type-Based Props Declaration)

这是 Vue 3 推荐的写法,它充分利用了 TypeScript 的类型系统,可以提供更精确的类型检查和更好的代码提示。

import { defineComponent, PropType } from 'vue';

interface Props {
  name: string;
  age?: number; // 可选属性
  isActive: boolean;
  profile: {
    nickname: string;
    level: number;
  }
}

export default defineComponent({
  props: {
    name: {
      type: String as PropType<string>, // as PropType<string> 很重要
      required: true,
    },
    age: {
      type: Number as PropType<number>,
      default: 18,
    },
    isActive: {
      type: Boolean as PropType<boolean>,
      default: false,
    },
    profile:{
        type: Object as PropType<Props['profile']>,
        required: true
    }
  },
  setup(props) {
    console.log(props.name); // 类型推断为 string
    console.log(props.age);  // 类型推断为 number
    console.log(props.isActive); // 类型推断为 boolean
    console.log(props.profile.nickname); // 类型推断为 string

    return {};
  },
});

在这个例子中,我们首先定义了一个 Props 接口,用来描述组件的 props 的类型。然后,在 props 选项中,我们使用 as PropType<类型> 来明确指定每个属性的类型。

重点:as PropType<类型>

这个 as PropType<类型> 非常重要! 它可以告诉 TypeScript,type 属性对应的类型是什么。如果没有它,TypeScript 可能无法正确推断类型,导致类型检查失效。

更简洁的写法:defineProps (从 Vue 3.3 开始)

Vue 3.3 引入了一个新的 API:defineProps。它可以让你更简洁地定义 props 的类型。

import { defineComponent, defineProps } from 'vue';

interface Props {
  name: string;
  age?: number; // 可选属性
  isActive: boolean;
  profile: {
    nickname: string;
    level: number;
  }
}

export default defineComponent({
  setup() {
    const props = defineProps<Props>();

    console.log(props.name); // 类型推断为 string
    console.log(props.age);  // 类型推断为 number
    console.log(props.isActive); // 类型推断为 boolean
    console.log(props.profile.nickname); // 类型推断为 string

    return {};
  },
});

使用 defineProps,你可以直接传入一个类型参数,TypeScript 会自动推断 props 的类型。这种方式更加简洁,也更符合 TypeScript 的编程习惯。

3. 混合方式 (Hybrid Approach)

有时候,你可能需要结合运行时声明和基于类型的声明。比如,你希望使用运行时声明来指定 default 值,同时又希望利用 TypeScript 进行类型检查。

import { defineComponent, PropType } from 'vue';

interface Props {
  name: string;
  age?: number;
}

export default defineComponent({
  props: {
    name: {
      type: String as PropType<string>,
      required: true,
    },
    age: {
      type: Number as PropType<number>,
      default: 18, // 运行时声明
    },
  },
  setup(props) {
    console.log(props.name); // 类型推断为 string
    console.log(props.age);  // 类型推断为 number

    return {};
  },
});

这种方式比较灵活,可以根据实际情况选择合适的声明方式。

emits 的类型定义:事件也要安排明白

接下来,咱们来看看 emits 的类型定义。和 props 类似,emits 也有两种主要的定义方式:

  1. 运行时声明 (Runtime Emits Declaration)
  2. 基于类型的声明 (Type-Based Emits Declaration)

1. 运行时声明 (Runtime Emits Declaration)

这种方式和 props 的运行时声明类似,使用对象字面量来声明事件,并指定它们的类型。

import { defineComponent } from 'vue';

export default defineComponent({
  emits: ['update', 'delete', 'custom-event'],
  setup(props, { emit }) {
    emit('update', 123); // 触发 update 事件,传递一个 number 类型的值
    emit('delete');      // 触发 delete 事件,不传递任何值
    emit('custom-event', 'hello', 456); //触发自定义事件
    return {};
  },
});

这种方式的优点是简单易懂。缺点是,TypeScript 无法对事件的参数进行类型检查。也就是说,即使你传递了错误的参数类型,TypeScript 也不会报错。

2. 基于类型的声明 (Type-Based Emits Declaration)

为了解决运行时声明的不足,Vue 3 提供了基于类型的 emits 声明方式。

import { defineComponent, EmitsOptions } from 'vue';

interface Emits {
  (e: 'update', value: number): void;
  (e: 'delete'): void;
  (e: 'custom-event', message: string, id: number): void;
}

export default defineComponent({
  emits: ['update', 'delete', 'custom-event'], // 运行时声明,用于注册事件
  setup(props, { emit }) {
    const emitTyped = emit as Emits; // 类型断言

    emitTyped('update', 123); // 正确
    // emitTyped('update', 'abc'); // 错误!参数类型不匹配

    emitTyped('delete');      // 正确

    emitTyped('custom-event', 'hello', 456); //正确
    // emitTyped('custom-event', 123, 'abc'); //错误! 参数类型不匹配

    return {};
  },
});

在这个例子中,我们首先定义了一个 Emits 接口,用来描述组件可以 emit 的事件类型。然后,在 setup 函数中,我们将 emit 函数类型断言为 Emits 类型。这样,TypeScript 就可以对事件的参数进行类型检查了。

更简洁的写法:defineEmits (从 Vue 3.3 开始)

defineProps 类似,Vue 3.3 也引入了一个新的 API:defineEmits。它可以让你更简洁地定义 emits 的类型。

import { defineComponent, defineEmits } from 'vue';

interface Emits {
  (e: 'update', value: number): void;
  (e: 'delete'): void;
  (e: 'custom-event', message: string, id: number): void;
}

export default defineComponent({
  setup() {
    const emit = defineEmits<Emits>();

    emit('update', 123); // 正确
    // emit('update', 'abc'); // 错误!参数类型不匹配

    emit('delete');      // 正确
    emit('custom-event', 'hello', 456); //正确
    //emit('custom-event', 123, 'abc'); //错误! 参数类型不匹配

    return {};
  },
});

使用 defineEmits,你可以直接传入一个类型参数,TypeScript 会自动推断 emit 函数的类型。这种方式更加简洁,也更符合 TypeScript 的编程习惯。

更进一步:验证 emits 的参数

Vue 3 允许你通过 emits 选项中的函数来验证事件的参数。这可以进一步提高代码的可靠性。

import { defineComponent } from 'vue';

export default defineComponent({
  emits: {
    update: (value: number) => {
      // 验证 value 是否是数字类型
      return typeof value === 'number';
    },
    'custom-event': (message: string, id: number) => {
      // 验证 message 是否是字符串类型,id 是否是数字类型
      return typeof message === 'string' && typeof id === 'number';
    },
    delete:null
  },
  setup(props, { emit }) {
    emit('update', 123); // 正确
    // emit('update', 'abc'); // 错误!参数验证失败

    emit('delete');      // 正确
    emit('custom-event', 'hello', 456); //正确
    //emit('custom-event', 123, 'abc'); //错误! 参数验证失败
    return {};
  },
});

在这个例子中,我们为 update 事件定义了一个验证函数,它会检查 value 是否是数字类型。如果验证失败,Vue 会在控制台中发出警告。

总结:选择最适合你的方式

总的来说,propsemits 的类型定义方式有很多种,你可以根据自己的需求和偏好选择最适合你的方式。

  • 运行时声明: 简单易懂,但类型检查能力有限。
  • 基于类型的声明: 可以提供更精确的类型检查和更好的代码提示,但需要更多的代码。
  • 混合方式: 灵活,可以结合运行时声明和基于类型的声明的优点。
  • definePropsdefineEmits Vue 3.3 引入的新 API,更加简洁,也更符合 TypeScript 的编程习惯。
特性 运行时声明 基于类型的声明 混合方式 defineProps/defineEmits(Vue 3.3+)
类型检查 有限的类型检查(基于 type 属性) 更精确的类型检查(基于 TypeScript 类型系统) 灵活,可以结合两种方式的优点 非常精确,完全依赖 TypeScript 类型系统
代码简洁性 相对简单 相对复杂 中等 非常简洁
适用场景 小型项目,或者对类型检查要求不高的场景 中大型项目,或者对类型检查要求较高的场景 需要同时使用运行时声明和基于类型的声明的场景 推荐使用,尤其是对于大型项目,可以提升开发效率和代码质量
优点 易于理解,上手快 类型安全,代码提示,可维护性高 灵活,可以根据实际情况选择合适的声明方式 简洁,类型安全,代码提示,可维护性高
缺点 类型检查能力有限 代码量相对较多 可能导致代码结构混乱 需要对 TypeScript 有一定的了解
示例代码 (props) props: { name: { type: String } } props: { name: { type: String as PropType<string> } } props: { name: { type: String as PropType<string>, default: 'default name' } } (运行时声明 default,类型声明类型) const props = defineProps<{ name: string }>()
示例代码 (emits) emits: ['update'] emits: { update: (value: number) => boolean } 不适用 (混合方式主要用于props) const emit = defineEmits<{ (e: 'update', value: number): void }>()

最后的叮嘱:实践出真知

好了,今天的 Vue 3 源码极客小课堂就到这里了。希望通过今天的讲解,你对 propsemits 的类型定义有了更深入的理解。

记住,纸上得来终觉浅,绝知此事要躬行。只有在实际项目中不断练习,才能真正掌握这些知识。

祝大家编程愉快,bug 远离!我们下期再见!

发表回复

您的邮箱地址不会被公开。 必填项已用 * 标注