Skip to content
第 128 / 250 章前端⏱ 10 分钟阅读

第 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({});    // false

6.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) => void

11.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>

十六、本章小结

概念关键
tsconfigstrict + 路径别名
defineProps泛型 + withDefaults
defineEmits元组形式
templateRef显式类型
InstanceType组件 ref 类型
InjectionKeyprovide/inject 类型
泛型 composableT 推断 + 默认值
声明文件.d.ts

动手练习

  1. 类型化 props:为你的组件添加完整的类型定义
  2. 泛型组件:编写一个支持任意类型的 List<T> 组件
  3. 类型化 store:用 Pinia Setup Store + 完整类型
  4. 环境变量:为不同环境定义类型
  5. 声明文件:为第三方无类型库添加声明

推荐阅读


下一章第 129 章:组件库设计

本站基于 VitePress 构建 · 由 Codebook 团队维护