TypeScript 深度集成
Vue 3 源码使用 TypeScript 编写,<script setup lang="ts"> 可获得良好的类型推导。见 Vue 基础。
Vue 3 采用 TypeScript 编写,天然提供了优秀的类型支持。组合式 API 加上 <script setup> 语法糖,使得类型推导更加智能。下面从组件开发、依赖注入、全局扩展、状态管理、组合式函数等角度介绍最佳实践。
1. 组件 Props 类型定义
在 <script setup> 中,使用纯类型语法定义 props,无需运行时声明。
基本泛型语法
<script setup lang="ts">
// 编译时转换为运行时声明,同时提供类型
const props = defineProps<{
title: string;
count: number;
visible?: boolean; // 可选
tags: string[];
}>();
</script>
withDefaults 提供默认值
当需要默认值时,使用 withDefaults 编译器宏:
<script setup lang="ts">
export interface Props {
title: string;
count?: number;
disabled?: boolean;
}
const props = withDefaults(defineProps<Props>(), {
count: 0,
disabled: false,
});
</script>
复杂类型
- 联合类型:
status: 'loading' | 'success' | 'error' - 接口引用:先定义
interface User { name: string; age: number },然后user: User - 函数类型 props:
onClick?: (value: string) => void
defineProps<{
onSubmit: (data: FormData) => Promise<void>;
}>();
注意:类型仅用于编译时,运行时 props 验证依然由 Vue 处理(通过 runtimeProps 转换)。如果需要在运行时检查复杂类型,可搭配 validator,但类型系统本身已足够。
2. 组件 Emits 类型定义
使用 defineEmits 并标注事件签名,可以获得类型提示和校验。
<script setup lang="ts">
const emit = defineEmits<{
(e: "change", value: string): void;
(e: "update", id: number, payload: Record<string, any>): void;
(e: "click", event: MouseEvent): void;
}>();
// 调用时自动推导参数类型
emit("change", "hello"); // ✅
emit("update", 1, { a: 1 }); // ✅
emit("click", new MouseEvent("click")); // ✅
</script>
对于更简洁的写法(Vue 3.3+),可以使用字面量语法:
const emit = defineEmits<{
change: [value: string];
update: [id: number, payload: Record<string, any>];
}>();
3. 模板 ref 的类型标注
当通过 ref 获取子组件实例或 DOM 元素时,需要标注正确的类型。
DOM 元素
<script setup lang="ts">
import { ref, onMounted } from "vue";
const inputRef = ref<HTMLInputElement | null>(null);
onMounted(() => {
inputRef.value?.focus(); // 使用可选链
});
</script>
<template>
<input ref="inputRef" />
</template>
子组件实例
<!-- Child.vue -->
<script setup lang="ts">
defineExpose({
reset: () => {
/* ... */
},
});
</script>
<!-- Parent.vue -->
<script setup lang="ts">
import Child from "./Child.vue";
const childRef = ref<InstanceType<typeof Child> | null>(null);
function handleReset() {
childRef.value?.reset();
}
</script>
<template>
<Child ref="childRef" />
</template>
4. 依赖注入的类型安全:InjectionKey<T>
使用 InjectionKey 声明注入值的类型,避免 inject 返回 unknown。
// types.ts
import { InjectionKey, Ref } from "vue";
export interface User {
name: string;
age: number;
}
export const userKey: InjectionKey<Ref<User>> = Symbol("user");
export const updateUserKey: InjectionKey<(user: User) => void> = Symbol("updateUser");
<!-- Provider.vue -->
<script setup lang="ts">
import { provide, ref } from "vue";
import { userKey, updateUserKey } from "./types";
const user = ref({ name: "Alice", age: 25 });
const updateUser = (newUser: User) => {
user.value = newUser;
};
provide(userKey, user);
provide(updateUserKey, updateUser);
</script>
<!-- Consumer.vue -->
<script setup lang="ts">
import { inject } from "vue";
import { userKey, updateUserKey } from "./types";
const user = inject(userKey); // 类型为 Ref<User> | undefined
const updateUser = inject(updateUserKey); // (user: User) => void | undefined
if (user && updateUser) {
console.log(user.value.name);
updateUser({ name: "Bob", age: 30 });
}
</script>
5. 全局类型扩展:ComponentCustomProperties
当通过插件挂载全局属性(如 $axios)或修改全局组件选项时,需要扩展 Vue 的类型接口。
// main.ts
import axios from "axios";
const app = createApp(App);
app.config.globalProperties.$axios = axios;
// shims-vue.d.ts 或 global.d.ts
import axios from "axios";
declare module "vue" {
interface ComponentCustomProperties {
$axios: typeof axios;
$translate: (key: string) => string;
}
}
之后在组合式 API 中可以通过 getCurrentInstance() 访问,但推荐使用依赖注入或 composable 封装。在模板中可以直接使用 {{ $axios }}(不推荐)。
6. Pinia store 的 TypeScript 最佳实践
Pinia 对 TypeScript 支持极佳,尤其是 Setup store(推荐)会自动推导类型。
Setup Store(自动类型推导)
// stores/user.ts
import { defineStore } from "pinia";
import { ref, computed } from "vue";
export const useUserStore = defineStore("user", () => {
const name = ref("Alice");
const age = ref(25);
const isAdult = computed(() => age.value >= 18);
function setAge(newAge: number) {
age.value = newAge;
}
return { name, age, isAdult, setAge };
});
在组件中使用时,所有属性和方法都有完整类型:
<script setup lang="ts">
import { useUserStore } from "@/stores/user";
const userStore = useUserStore();
userStore.setAge(30); // ✅ 参数类型为 number
console.log(userStore.isAdult); // boolean
</script>
Options Store 类型标注
若使用 Options Store,需手动标注类型:
interface UserState {
name: string;
age: number;
}
export const useUserStore = defineStore("user", {
state: (): UserState => ({ name: "Alice", age: 25 }),
getters: {
isAdult: (state) => state.age >= 18,
},
actions: {
setAge(age: number) {
this.age = age;
},
},
});
跨 store 类型引用
import { useCartStore } from "./cart";
const cart = useCartStore();
// cart 具有完整类型
7. 组合式函数的类型设计
设计 Composable(组合式函数)时,应提供清晰的返回类型声明和泛型约束。
显式返回类型声明
import { ref, Ref } from "vue";
interface UseCounterReturn {
count: Ref<number>;
increment: () => void;
reset: () => void;
}
export function useCounter(initial: number = 0): UseCounterReturn {
const count = ref(initial);
const increment = () => count.value++;
const reset = () => {
count.value = initial;
};
return { count, increment, reset };
}
泛型组合式函数
import { ref, Ref, unref, MaybeRef } from "vue";
// 约束 T 为某个类型,返回值使用泛型
export function useFetch<T>(url: MaybeRef<string>): {
data: Ref<T | null>;
error: Ref<Error | null>;
loading: Ref<boolean>;
} {
const data = ref<T | null>(null);
const error = ref<Error | null>(null);
const loading = ref(false);
// ... 实现
return { data, error, loading };
}
// 使用时指定类型
const { data } = useFetch<User[]>("/api/users");
// data 类型为 Ref<User[] | null>
处理 MaybeRef 参数
Vue 3.3+ 提供了 toValue 辅助函数,可统一处理 Ref 或普通值:
import { toValue, type MaybeRefOrGetter } from "vue";
export function useDouble(value: MaybeRefOrGetter<number>) {
const double = computed(() => toValue(value) * 2);
return { double };
}
8. 总结
| 场景 | 做法 | 收益 |
|---|---|---|
| Props | defineProps<{...}>() + withDefaults | 编译时类型检查,自动生成运行时声明 |
| Emits | defineEmits<{(e: 'change', v: string): void}>() | 事件参数类型校验,调用提示 |
| 模板 ref | ref<HTMLInputElement>, ref<InstanceType<typeof Comp>> | 安全访问 DOM / 组件实例 |
| 依赖注入 | InjectionKey<T> + provide/inject | 类型安全的跨层级通信 |
| 全局属性 | ComponentCustomProperties 扩展 | 为 this.$xxx 或模板中的全局变量提供类型 |
| Pinia | Setup Store 自动推导 | 几乎零配置类型安全 |
| Composables | 显式返回类型、泛型约束、MaybeRef 参数 | 可复用逻辑的类型安全与灵活性 |
借助 TypeScript,Vue 3 的开发体验大幅提升,不仅减少了运行时错误,还让重构和协作更加可靠。建议在项目中全面启用 TypeScript,并遵循上述模式。
参考文献
以下链接在编写时均可正常访问:
| 资料 | 说明 |
|---|---|
| Vue:TypeScript 支持 | 总览 |
| Vue:<script setup> + TS | 组合式 API |
| vue-tsc | 类型检查 CLI |
相关文章
资源优化
图片懒加载、按需引入与 Web Vitals 监控。见 HTML 图片、CSS 字体优化。
状态管理:Pinia 与 Vuex
Pinia 是 Vue 官方推荐的状态管理库(Vue 3 新项目默认选型)。Vuex 4 仍可与 Vue 3 配合,用于维护旧项目。见 Vue 基础。
Vue 生态关键库深度集成
VueUse、Vite 与 Nuxt 是 Vue 3 生态中常用的效率与工程化方案。概览见 Vue 基础。
依赖收集与派发更新机制
说明 Vue 3 中 track / trigger 与 targetMap 的工作方式。前置:v2-v3 差异。
测试策略
Vue 应用常用 Vitest + @vue/test-utils 做单元/组件测试,Playwright 或 Cypress 做 E2E。见 Vue 基础。
组合式 API 与逻辑复用设计模式
组合式 API 与 Composables 是 Vue 3 推荐的逻辑组织方式。概览见 Vue 基础。
Series
vue
15 / 16