第 128 章:TypeScript 深度集成
学习目标
- 掌握 Vue 3 + TypeScript 的配置
- 学会组件 props、emits、slots 的类型化
- 理解 ref、reactive、computed 的类型推断
- 在项目中处理复杂类型场景
一、为什么需要 TypeScript
Vue 3 本身就是用 TypeScript 编写的,Composition API 对 TS 友好,大型项目必备。
二、项目配置
2.1 tsconfig.json
json
{
"compilerOptions": {
"target": "ESNext",
"module": "ESNext",
"moduleResolution": "Bundler",
"strict": true,
"jsx": "preserve",
"sourceMap": true,
"resolveJsonModule": true,
"esModuleInterop": true,
"lib": ["ESNext", "DOM"],
"skipLibCheck": true,
"noUnusedLocals": true,
"noUnusedParameters": true,
"baseUrl": ".",
"paths": {
"@/*": ["src/*"]
},
"types": ["vite/client"]
},
"include": ["src/**/*", "src/**/*.vue", "vite.config.ts"]
}2.2 Vite 配置
typescript
// vite.config.ts
import { defineConfig } from 'vite';
import vue from '@vitejs/plugin-vue';
import path from 'path';
export default defineConfig({
plugins: [vue()],
resolve: {
alias: {
'@': path.resolve(__dirname, 'src')
}
}
});三、组件 Props 类型
3.1 基础类型
vue
<script setup lang="ts">
// 简单类型
const props = defineProps<{
msg: string;
count: number;
active?: boolean;
}>();
</script>3.2 复杂类型
vue
<script setup lang="ts">
interface User {
id: number;
name: string;
email: string;
}
interface Props {
user: User;
users: User[];
callback: (id: number) => void;
config?: Record<string, string>;
}
const props = defineProps<Props>();
</script>3.3 联合类型
vue
<script setup lang="ts">
type Size = 'small' | 'medium' | 'large';
type Variant = 'primary' | 'secondary' | 'danger';
const props = defineProps<{
size: Size;
variant?: Variant;
}>();
</script>3.4 withDefaults
vue
<script setup lang="ts">
interface Props {
msg: string;
visible?: boolean;
items?: string[];
}
const props = withDefaults(defineProps<Props>(), {
visible: false,
items: () => ['default'] // 工厂函数返回数组/对象
});
</script>四、Emits 类型
4.1 基础
vue
<script setup lang="ts">
const emit = defineEmits<{
change: [value: string];
update: [id: number, value: string];
}>();
emit('change', 'hello');
emit('update', 1, 'foo');
</script>4.2 验证
vue
<script setup lang="ts">
const emit = defineEmits<{
// 元组形式,第二项是验证函数
submit: [payload: { name: string }];
// null 表示不验证
close: [];
}>();
</script>4.3 事件名
vue
<script setup lang="ts">
const emit = defineEmits<{
'update:value': [val: string];
'update:visible': [val: boolean];
}>();
// v-model:visible 使用
</script>五、Slots 类型
5.1 defineSlots
vue
<script setup lang="ts">
defineSlots<{
default(props: { item: Item }): any;
header(): any;
footer(props: { message: string }): any;
}>();
</script>
<template>
<div>
<slot name="header" />
<slot :item="data" />
<slot name="footer" :message="msg" />
</div>
</template>5.2 类型安全的 slot
vue
<!-- Parent.vue -->
<script setup lang="ts">
import List from './List.vue';
const items = ref([1, 2, 3]);
</script>
<template>
<List :items="items">
<template #default="{ item }">
<!-- item: number -->
{{ item }}
</template>
</List>
</template>六、Ref 与 Reactive 类型
6.1 ref 类型推断
typescript
import { ref } from 'vue';
// 自动推断
const count = ref(0); // Ref<number>
const name = ref('Tom'); // Ref<string>
const list = ref([1, 2, 3]); // Ref<number[]>
// 显式标注
const user = ref<User | null>(null);
const loading = ref<boolean>(false);6.2 reactive 类型
typescript
import { reactive } from 'vue';
interface State {
count: number;
user: User | null;
}
const state = reactive<State>({
count: 0,
user: null
});6.3 unref / isRef / isReactive
typescript
import { unref, isRef, isReactive } from 'vue';
const r = ref(0);
unref(r); // 0
isRef(r); // true
isReactive({}); // false6.4 templateRef 类型
vue
<script setup lang="ts">
import { ref, onMounted } from 'vue';
const elRef = ref<HTMLDivElement | null>(null);
const inputRef = ref<HTMLInputElement | null>(null);
onMounted(() => {
elRef.value?.focus();
});
</script>
<template>
<div ref="elRef">...</div>
<input ref="inputRef" />
</template>6.5 组件 ref 类型
vue
<script setup lang="ts">
import { ref } from 'vue';
import MyComponent from './MyComponent.vue';
const compRef = ref<InstanceType<typeof MyComponent> | null>(null);
const handleClick = () => {
compRef.value?.someMethod();
console.log(compRef.value?.someState);
};
</script>
<template>
<MyComponent ref="compRef" />
</template>七、Computed 与 Watch
7.1 computed
typescript
import { ref, computed } from 'vue';
const count = ref(0);
const doubled = computed(() => count.value * 2); // ComputedRef<number>
// 显式
const user = ref<User | null>(null);
const userName = computed(() => user.value?.name); // ComputedRef<string | undefined>7.2 可写 computed
typescript
const firstName = ref('Tom');
const lastName = ref('Jerry');
const fullName = computed({
get: () => `${firstName.value} ${lastName.value}`,
set: (val: string) => {
const parts = val.split(' ');
firstName.value = parts[0];
lastName.value = parts[1];
}
});7.3 watch 类型
typescript
import { ref, watch } from 'vue';
const count = ref(0);
const user = ref<User | null>(null);
// 监听 ref
watch(count, (newVal: number, oldVal: number) => {});
// 监听 getter
watch(
() => user.value?.name,
(newName: string | undefined) => {}
);
// 监听多个源
watch(
[count, () => user.value?.name],
([newCount, newName], [oldCount, oldName]) => {}
);八、Provide / Inject 类型
8.1 InjectionKey
typescript
// keys.ts
import type { InjectionKey, Ref } from 'vue';
export interface User {
id: number;
name: string;
}
export const UserKey: InjectionKey<Ref<User | null>> = Symbol('user');8.2 类型安全注入
vue
<!-- Parent.vue -->
<script setup lang="ts">
import { ref, provide } from 'vue';
import { UserKey, type User } from './keys';
const user = ref<User | null>(null);
provide(UserKey, user);
</script>
<!-- Child.vue -->
<script setup lang="ts">
import { inject } from 'vue';
import { UserKey } from './keys';
// 类型:Ref<User | null> | undefined
const user = inject(UserKey);
// 提供默认值
const user2 = inject(UserKey, ref<User | null>(null));
// 类型:Ref<User | null>
</script>九、组合式函数类型
9.1 简单返回
typescript
import { ref, type Ref } from 'vue';
export function useCounter(initial = 0): {
count: Ref<number>;
increment: () => void;
} {
const count = ref(initial);
const increment = () => count.value++;
return { count, increment };
}9.2 泛型
typescript
export function useFetch<T>(url: string): {
data: Ref<T | null>;
loading: Ref<boolean>;
error: Ref<Error | null>;
execute: () => Promise<void>;
} {
const data = ref<T | null>(null);
const loading = ref(false);
const error = ref<Error | null>(null);
async function execute() {
loading.value = true;
try {
const res = await fetch(url);
data.value = await res.json();
} catch (e) {
error.value = e as Error;
} finally {
loading.value = false;
}
}
return { data, loading, error, execute };
}
// 使用
const { data } = useFetch<User[]>('/api/users');
// data.value: User[] | null ✅9.3 MaybeRef
typescript
import type { Ref } from 'vue';
type MaybeRef<T> = T | Ref<T>;
export function useTitle(title: MaybeRef<string>) {
// 自动转换
}9.4 函数重载
typescript
// 重载 1:ref
export function useDebounce<T>(value: Ref<T>, delay?: number): Ref<T>;
// 重载 2:getter
export function useDebounce<T>(getter: () => T, delay?: number): Ref<T>;
// 实现
export function useDebounce(value: any, delay = 300) {
// ...
}十、Vue Router 类型
10.1 路由元
typescript
// types/router.d.ts
import 'vue-router';
declare module 'vue-router' {
interface RouteMeta {
requiresAuth?: boolean;
title?: string;
roles?: Array<'admin' | 'user'>;
}
}10.2 路由参数
typescript
declare module 'vue-router' {
interface RouteParams {
id?: string;
}
}十一、Pinia 类型
11.1 Setup Store 自动推断
typescript
import { defineStore } from 'pinia';
import { ref, computed } from 'vue';
export const useUserStore = defineStore('user', () => {
const name = ref('Tom'); // Ref<string>
const age = ref(18); // Ref<number>
const isAdult = computed(() => age.value >= 18); // ComputedRef<boolean>
function login(token: string) {
// ...
}
return { name, age, isAdult, login };
});
const store = useUserStore();
store.name; // ✅ string
store.isAdult; // ✅ boolean
store.login; // ✅ (token: string) => void11.2 Options Store
typescript
interface State {
name: string;
age: number;
}
export const useUserStore = defineStore('user', {
state: (): State => ({ name: 'Tom', age: 18 }),
getters: {
isAdult: (state): boolean => state.age >= 18
},
actions: {
login(payload: LoginDTO): void {
// ...
}
}
});十二、环境变量类型
typescript
// src/env.d.ts
// 引用 vite 客户端类型
interface ImportMetaEnv {
readonly VITE_API_URL: string;
readonly VITE_APP_TITLE: string;
}
interface ImportMeta {
readonly env: ImportMetaEnv;
}typescript
// 使用
console.log(import.meta.env.VITE_API_URL); // 类型安全十三、声明文件
13.1 模块声明
typescript
// types/css.d.ts
declare module '*.css' {
const content: string;
export default content;
}
declare module '*.vue' {
import type { DefineComponent } from 'vue';
const component: DefineComponent<{}, {}, any>;
export default component;
}13.2 全局类型
typescript
// globals.d.ts
declare global {
interface Window {
__APP_CONFIG__: AppConfig;
}
}
export {};十四、常见类型陷阱
14.1 ref 解构丢失响应式
typescript
// ❌ 丢失
const { value } = ref({ count: 0 });
// ✅
const r = ref({ count: 0 });
r.value.count;14.2 reactive 不适合基本类型
typescript
// ❌ 不生效
const count = reactive(0); // 警告
// ✅
const count = ref(0);14.3 type-only 导入
vue
<script setup lang="ts">
// ❌ 运行时导入
import { User } from './types';
// ✅ 类型导入
import type { User } from './types';
</script>14.4 异步组件
typescript
import { defineAsyncComponent } from 'vue';
const AsyncComp = defineAsyncComponent(() => import('./Comp.vue'));十五、Vue 3.4+ 类型改进
15.1 简化的 defineProps
vue
<script setup lang="ts">
// 3.4 之前
const props = defineProps<{ msg: string }>();
// 3.4+ 简写
defineProps<{ msg: string }>();
</script>15.2 响应式 Props 解构
vue
<script setup lang="ts">
// 3.5+ 解构 props 仍保持响应式
const { msg, count } = defineProps<{
msg: string;
count: number;
}>();
// 模板直接用 {{ msg }}
</script>十六、本章小结
| 概念 | 关键 |
|---|---|
| tsconfig | strict + 路径别名 |
| defineProps | 泛型 + withDefaults |
| defineEmits | 元组形式 |
| templateRef | 显式类型 |
| InstanceType | 组件 ref 类型 |
| InjectionKey | provide/inject 类型 |
| 泛型 composable | T 推断 + 默认值 |
| 声明文件 | .d.ts |
动手练习
- 类型化 props:为你的组件添加完整的类型定义
- 泛型组件:编写一个支持任意类型的
List<T>组件 - 类型化 store:用 Pinia Setup Store + 完整类型
- 环境变量:为不同环境定义类型
- 声明文件:为第三方无类型库添加声明
推荐阅读
- 📖 Vue 3 TypeScript 指南 — 官方
- 📖 Vue Language Tools — Volar
- 🌐 Vue + TS 示例 — 范例
- 📖 TypeScript 手册 — TS 官方
下一章:第 129 章:组件库设计 →