第257集:Vue 3.3+类型系统增强

概述

Vue 3.3版本对TypeScript类型系统进行了重大增强,提供了更精确的类型推断、新的工具类型以及更好的.vue文件支持。这些增强使得Vue 3.3+在TypeScript开发体验方面有了显著提升,减少了类型标注的冗余,提高了类型安全性。本集将深入探讨这些类型系统增强特性,包括改进的组件类型推断、新的工具类型(ComponentProps、ComponentEmits、ComponentExposed)以及更好的.vue文件支持等内容,帮助开发者更好地理解和应用这些新特性。

改进的组件类型推断

1. Props类型推断增强

Vue 3.3改进了组件Props的类型推断,特别是在使用defineProps宏时:

<!-- Component.vue -->
<template>
  <div>{{ message }}</div>
</template>

<script setup lang="ts">
// Vue 3.3之前需要显式类型标注
const props = defineProps<{
  message: string
  count?: number
}>()

// Vue 3.3可以使用withDefaults进行默认值和类型推断
const props = withDefaults(defineProps<{
  message: string
  count?: number
}>(), {
  count: 0
})
</script>

2. Emits类型推断增强

Vue 3.3改进了defineEmits宏的类型推断,支持更复杂的事件类型:

<!-- Component.vue -->
<template>
  <button @click="$emit('click', { x: 10, y: 20 })">
    Click me
  </button>
</template>

<script setup lang="ts">
// Vue 3.3增强的Emits类型推断
const emit = defineEmits<{
  // 简单事件
  'update:modelValue': [value: string]
  // 复杂事件类型
  'click': [position: { x: number; y: number }]
  // 带有多个参数的事件
  'change': [oldValue: number, newValue: number]
  // 无参数事件
  'reset': []
}>()
</script>

3. Expose类型推断增强

Vue 3.3改进了defineExpose宏的类型推断,使得父组件可以获得更精确的子组件暴露类型:

<!-- ChildComponent.vue -->
<template>
  <div>{{ count }}</div>
</template>

<script setup lang="ts">
const count = ref(0)

const increment = () => {
  count.value++
}

const decrement = () => {
  count.value--
}

// 暴露组件内部状态和方法
defineExpose({
  count,
  increment,
  decrement
})
</script>
<!-- ParentComponent.vue -->
<template>
  <ChildComponent ref="childRef" />
  <button @click="childRef?.increment">Increment</button>
</template>

<script setup lang="ts">
import ChildComponent from './ChildComponent.vue'

// Vue 3.3自动推断ChildComponent的暴露类型
const childRef = ref<InstanceType<typeof ChildComponent> | null>(null)

// 可以获得精确的类型提示
childRef.value?.count // number
childRef.value?.increment() // () => void
</script>

新的工具类型

Vue 3.3引入了多个新的工具类型,用于更精确地类型标注:

1. ComponentProps

ComponentProps&lt;T&gt;用于获取组件的Props类型:

// 从组件获取Props类型
declare type ComponentProps<T extends Component> = T extends Component<infer P> ? P : never

使用示例

<!-- ButtonComponent.vue -->
<template>
  <button :class="{ primary, large }">
    <slot></slot>
  </button>
</template>

<script setup lang="ts">
defineProps<{
  primary?: boolean
  large?: boolean
  disabled?: boolean
}>()
</script>
<!-- FormComponent.vue -->
<template>
  <form>
    <!-- 使用ButtonComponent的Props类型 -->
    <ButtonComponent v-bind="buttonProps">
      Submit
    </ButtonComponent>
  </form>
</template>

<script setup lang="ts">
import { ref } from 'vue'
import ButtonComponent from './ButtonComponent.vue'

// 使用ComponentProps获取ButtonComponent的Props类型
const buttonProps = ref<ComponentProps<typeof ButtonComponent>>({
  primary: true,
  large: false,
  disabled: false
})
</script>

2. ComponentEmits

ComponentEmits&lt;T&gt;用于获取组件的Emits类型:

// 从组件获取Emits类型
declare type ComponentEmits<T extends Component> = T extends Component<any, infer E> ? E : never

使用示例

<!-- InputComponent.vue -->
<template>
  <input
    :value="modelValue"
    @input="$emit('update:modelValue', ($event.target as HTMLInputElement).value)"
    @focus="$emit('focus')"
    @blur="$emit('blur')"
  />
</template>

<script setup lang="ts">
defineProps<{
  modelValue: string
}>()

defineEmits<{
  'update:modelValue': [value: string]
  'focus': []
  'blur': []
}>()
</script>
// 使用ComponentEmits获取组件的事件类型
import InputComponent from './InputComponent.vue'

type InputEmits = ComponentEmits<typeof InputComponent>
// InputEmits = {
//   'update:modelValue': [value: string]
//   'focus': []
//   'blur': []
// }

3. ComponentExposed

ComponentExposed&lt;T&gt;用于获取组件通过defineExpose暴露的类型:

// 从组件获取暴露类型
declare type ComponentExposed<T extends Component> = T extends Component<any, any, infer E> ? E : never

使用示例

<!-- ModalComponent.vue -->
<template>
  <div v-if="isOpen" class="modal">
    <div class="modal-content">
      <slot></slot>
      <button @click="close">Close</button>
    </div>
  </div>
</template>

<script setup lang="ts">
const isOpen = ref(false)

const open = () => {
  isOpen.value = true
}

const close = () => {
  isOpen.value = false
}

defineExpose({
  isOpen,
  open,
  close
})
</script>
<!-- App.vue -->
<template>
  <button @click="openModal">Open Modal</button>
  <ModalComponent ref="modalRef">
    <h2>Modal Content</h2>
  </ModalComponent>
</template>

<script setup lang="ts">
import ModalComponent from './ModalComponent.vue'

// 使用ComponentExposed获取ModalComponent的暴露类型
const modalRef = ref<ComponentExposed<typeof ModalComponent> | null>(null)

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

4. ComponentInstance

ComponentInstance&lt;T&gt;用于获取组件实例的完整类型:

// 从组件获取实例类型
declare type ComponentInstance<T extends Component> = T extends new () => infer I ? I : never

使用示例

<!-- App.vue -->
<template>
  <div>
    <h1>Component Instance Type</h1>
  </div>
</template>

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

// 使用ComponentInstance获取完整的组件实例类型
const childInstance = ref<ComponentInstance<typeof ChildComponent> | null>(null)

onMounted(() => {
  // 可以访问组件实例的所有属性和方法
  console.log(childInstance.value?.$props)
  console.log(childInstance.value?.$emit)
  console.log(childInstance.value?.$el)
})
</script>

更好的.vue文件支持

Vue 3.3改进了对.vue文件的TypeScript支持,包括:

1. 改进的SFC类型推断

Vue 3.3改进了单文件组件(SFC)的类型推断,使得TypeScript可以更好地理解.vue文件的内容:

<!-- Component.vue -->
<template>
  <div :class="classes">{{ message }}</div>
</template>

<script setup lang="ts">
import { computed } from 'vue'

const props = defineProps<{
  message: string
  primary?: boolean
}>()

// 计算属性的类型推断
const classes = computed(() => {
  return {
    'primary': props.primary
  }
})
</script>

<style scoped>
.primary {
  color: blue;
}
</style>

2. 改进的模板类型检查

Vue 3.3改进了模板的类型检查,提供了更精确的错误提示:

<!-- Component.vue -->
<template>
  <!-- Vue 3.3会提示错误:count is possibly undefined -->
  <div>{{ count.toFixed(2) }}</div>
  
  <!-- Vue 3.3会提示错误:method does not exist -->
  <button @click="nonExistentMethod">Click</button>
</template>

<script setup lang="ts">
defineProps<{
  count?: number
}>()
</script>

3. 支持模板中的泛型组件

Vue 3.3支持在模板中使用泛型组件,并提供正确的类型推断:

<!-- GenericList.vue -->
<template>
  <ul>
    <li v-for="item in items" :key="item.id">
      <slot :item="item"></slot>
    </li>
  </ul>
</template>

<script setup lang="ts" generic="T extends { id: any }">
defineProps<{
  items: T[]
}>()
</script>
<!-- App.vue -->
<template>
  <!-- Vue 3.3支持模板中的泛型组件 -->
  <GenericList :items="users">
    <!-- 获得正确的item类型推断 -->
    <template #default="{ item }">
      {{ item.name }} - {{ item.email }}
    </template>
  </GenericList>
</template>

<script setup lang="ts">
import { ref } from 'vue'
import GenericList from './GenericList.vue'

interface User {
  id: number
  name: string
  email: string
}

const users = ref<User[]>([
  { id: 1, name: '张三', email: 'zhangsan@example.com' },
  { id: 2, name: '李四', email: 'lisi@example.com' }
])
</script>

类型系统最佳实践

1. 始终使用类型标注

尽管Vue 3.3提供了更好的类型推断,但始终为组件的Props和Emits添加显式类型标注:

<!-- 好的实践:显式类型标注 -->
<script setup lang="ts">
defineProps<{
  message: string
  count?: number
}>()

defineEmits<{
  'update:modelValue': [value: string]
}>()
</script>

<!-- 不好的实践:依赖隐式类型推断 -->
<script setup lang="ts">
// 缺少类型标注
const props = defineProps({
  message: String,
  count: { type: Number, default: 0 }
})
</script>

2. 使用工具类型简化代码

使用Vue 3.3提供的工具类型简化类型标注:

<!-- 好的实践:使用工具类型 -->
<script setup lang="ts">
import ButtonComponent from './ButtonComponent.vue'

// 使用ComponentProps简化类型标注
const buttonProps = ref<ComponentProps<typeof ButtonComponent>>({})
</script>

<!-- 不好的实践:重复定义类型 -->
<script setup lang="ts">
// 重复定义ButtonComponent的Props类型
const buttonProps = ref<{
  primary?: boolean
  large?: boolean
  disabled?: boolean
}>({})
</script>

3. 为复杂类型创建接口

为复杂的Props和Emits类型创建TypeScript接口:

<!-- 好的实践:使用接口 -->
<script setup lang="ts">
interface User {
  id: number
  name: string
  email: string
}

interface TableProps {
  data: User[]
  columns: Array<{
    key: keyof User
    label: string
  }>
  onRowClick?: (user: User, index: number) => void
}

defineProps<TableProps>()
</script>

<!-- 不好的实践:内联复杂类型 -->
<script setup lang="ts">
defineProps<{
  data: Array<{
    id: number
    name: string
    email: string
  }>
  columns: Array<{
    key: 'id' | 'name' | 'email'
    label: string
  }>
  onRowClick?: (user: {
    id: number
    name: string
    email: string
  }, index: number) => void
}>()
</script>

4. 利用withDefaults设置默认值

使用withDefaults宏同时设置默认值和类型:

<!-- 好的实践:使用withDefaults -->
<script setup lang="ts">
const props = withDefaults(defineProps<{
  message: string
  count?: number
  enabled?: boolean
}>(), {
  count: 0,
  enabled: true
})
</script>

<!-- 不好的实践:分开设置默认值和类型 -->
<script setup lang="ts">
const props = defineProps<{
  message: string
  count?: number
  enabled?: boolean
}>()

// 手动设置默认值
const effectiveCount = props.count ?? 0
const effectiveEnabled = props.enabled ?? true
</script>

类型系统调试技巧

1. 使用typeof获取类型

使用typeof操作符获取变量的类型:

<template>
  <div>
    <h1>Debugging Type System</h1>
  </div>
</template>

<script setup lang="ts">
const count = ref(0)
const message = ref('Hello')

// 使用typeof获取变量类型
let countType: typeof count // Ref<number>
let messageType: typeof message // Ref<string>
</script>

2. 使用类型断言

在必要时使用类型断言:

<template>
  <div>
    <h1>Type Assertion</h1>
  </div>
</template>

<script setup lang="ts">
const fetchData = async () => {
  const response = await fetch('/api/data')
  const data = await response.json()
  // 使用类型断言
  return data as { id: number; name: string }
}
</script>

3. 使用ts-expect-error注释

在已知类型检查会失败但代码正确的情况下,使用// @ts-expect-error注释:

<template>
  <div>
    <h1>TS Expect Error</h1>
  </div>
</template>

<script setup lang="ts">
// @ts-expect-error - 已知类型检查会失败但代码正确
const result = someFunctionThatReturnsUnknown()
</script>

总结

Vue 3.3+对TypeScript类型系统的增强显著提升了开发者体验,主要包括:

  1. 改进的组件类型推断

    • Props类型推断增强
    • Emits类型推断增强
    • Expose类型推断增强
  2. 新的工具类型

    • ComponentProps:获取组件的Props类型
    • ComponentEmits:获取组件的Emits类型
    • ComponentExposed:获取组件暴露的类型
    • ComponentInstance:获取组件实例的完整类型
  3. 更好的.vue文件支持

    • 改进的SFC类型推断
    • 改进的模板类型检查
    • 支持模板中的泛型组件

这些增强使得Vue 3.3+在TypeScript开发方面更加友好和高效,减少了类型标注的冗余,提高了类型安全性。通过合理应用这些新特性和最佳实践,开发者可以构建出类型安全、可维护性高的Vue应用。

在下一集中,我们将探讨Vue 3.3+中的编译器性能提升,包括解析速度、代码生成、静态hoisting和tree shaking等内容。

« 上一篇 Vue 3.3+响应式优化改进:提升性能与开发者体验 下一篇 » Vue 3.3+编译器性能提升:加快编译速度与运行时性能