FORMA

TypeScript 深度集成

Vue 3 源码使用 TypeScript 编写,<script setup lang="ts"> 可获得良好的类型推导。见 Vue 基础

Vue 3 采用 TypeScript 编写,天然提供了优秀的类型支持。组合式 API 加上 <script setup> 语法糖,使得类型推导更加智能。下面从组件开发、依赖注入、全局扩展、状态管理、组合式函数等角度介绍最佳实践。

1. 组件 Props 类型定义

<script setup> 中,使用纯类型语法定义 props,无需运行时声明。

基本泛型语法

vue
<script setup lang="ts">
// 编译时转换为运行时声明,同时提供类型
const props = defineProps<{
  title: string;
  count: number;
  visible?: boolean; // 可选
  tags: string[];
}>();
</script>

withDefaults 提供默认值

当需要默认值时,使用 withDefaults 编译器宏:

vue
<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
  • 函数类型 propsonClick?: (value: string) => void
ts
defineProps<{
  onSubmit: (data: FormData) => Promise<void>;
}>();

注意:类型仅用于编译时,运行时 props 验证依然由 Vue 处理(通过 runtimeProps 转换)。如果需要在运行时检查复杂类型,可搭配 validator,但类型系统本身已足够。

2. 组件 Emits 类型定义

使用 defineEmits 并标注事件签名,可以获得类型提示和校验。

vue
<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+),可以使用字面量语法:

ts
const emit = defineEmits<{
  change: [value: string];
  update: [id: number, payload: Record<string, any>];
}>();

3. 模板 ref 的类型标注

当通过 ref 获取子组件实例或 DOM 元素时,需要标注正确的类型。

DOM 元素

vue
<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>

子组件实例

vue
<!-- 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

ts
// 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");
vue
<!-- 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>
vue
<!-- 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 的类型接口。

ts
// main.ts
import axios from "axios";
const app = createApp(App);
app.config.globalProperties.$axios = axios;
ts
// 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(自动类型推导)

ts
// 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 };
});

在组件中使用时,所有属性和方法都有完整类型:

vue
<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,需手动标注类型:

ts
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 类型引用

ts
import { useCartStore } from "./cart";
const cart = useCartStore();
// cart 具有完整类型

7. 组合式函数的类型设计

设计 Composable(组合式函数)时,应提供清晰的返回类型声明和泛型约束。

显式返回类型声明

ts
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 };
}

泛型组合式函数

ts
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 或普通值:

ts
import { toValue, type MaybeRefOrGetter } from "vue";

export function useDouble(value: MaybeRefOrGetter<number>) {
  const double = computed(() => toValue(value) * 2);
  return { double };
}

8. 总结

场景做法收益
PropsdefineProps<{...}>() + withDefaults编译时类型检查,自动生成运行时声明
EmitsdefineEmits<{(e: 'change', v: string): void}>()事件参数类型校验,调用提示
模板 refref<HTMLInputElement>, ref<InstanceType<typeof Comp>>安全访问 DOM / 组件实例
依赖注入InjectionKey<T> + provide/inject类型安全的跨层级通信
全局属性ComponentCustomProperties 扩展this.$xxx 或模板中的全局变量提供类型
PiniaSetup Store 自动推导几乎零配置类型安全
Composables显式返回类型、泛型约束、MaybeRef 参数可复用逻辑的类型安全与灵活性

借助 TypeScript,Vue 3 的开发体验大幅提升,不仅减少了运行时错误,还让重构和协作更加可靠。建议在项目中全面启用 TypeScript,并遵循上述模式。

参考文献

以下链接在编写时均可正常访问:

资料说明
Vue:TypeScript 支持总览
Vue:<script setup> + TS组合式 API
vue-tsc类型检查 CLI