Vue 3 与 TypeScript 最佳实践

999 阅读6分钟

在 Vue 3 中结合 TypeScript,能够提供更强的类型检查和更好的开发体验。以下是一些最佳实践,可以帮助你有效地在 Vue 3 项目中使用 TypeScript

1. 使用 Vue CLI 或 Vite 创建项目

使用 Vue CLI 或 Vite 创建项目时,可以选择 TypeScript 模板,这样会自动配置 TypeScript 相关的依赖和配置文件。

# 使用 Vue CLI
vue create my-project --preset default
# 在配置中选择 TypeScript

# 使用 Vite
npm init vite@latest my-project --template vue-ts

2. 使用 <script setup> 语法

Vue 3 的 <script setup> 是一个更简洁的语法,可以大大简化组件的定义。使用时,TypeScript 能够更好地推断类型。

<template>
  <h1>{{ message }}</h1>
</template>

<script setup lang="ts">
import { ref, reactive, computed} from 'vue';
import type { Ref } from 'vue'  
// 泛型 T
const message = ref<string>('Hello, Vue 3 with TypeScript!');
// 通过接口指定类型
const countRef<string | number> = ref('1')  
count.value = 20

interface Person {  
  name: string  
  age?: number  
}
// 显式地指定一个 `reactive` 变量的类型 interface or type
const personPerson = reactive({ name'隔壁老王' })

// 自动推导得到的类型:ComputedRef<number>  
const double = computed(() => count.value * 2)

// 通过泛型指定类型
const double = computed<number>(() => {  
  // 若返回值不是 number 类型则会报错  
})
</script>

3. 声明类型

为 props、emit、和 data 声明明确的类型,以获得更好的类型检查。

<script setup lang="ts">
import { defineProps, defineEmits } from 'vue';

const props = defineProps<{
  title: string;
  count: number;
}>();

// `基于类型的声明` 可以使我们对所触发事件的类型进行更细粒度的控制
const emit = defineEmits<{
  (e: 'increment'): void;
}>();

// 使用 props
console.log(props.title);
</script>

或者

interface Props {  
  msg?: string  
}  
  
const props = withDefaults(defineProps<Props>(), {  
  msg'hello,Vue3'   
})

// 非 `<script setup>`
import { defineComponent } from 'vue'  
  
export default defineComponent({  
  props: {  
    msgString  
  },  
  setup(props) {  
    props.msg // <-- 类型:string  
  }  
})

// 非 `<script setup>`
import { defineComponent } from 'vue'  
  
export default defineComponent({  
  emits: ['change'],  
  setup(props, { emit }) {  
    emit('change'// <-- 类型检查 / 自动补全  
  }  
})

4. 使用 Vuex 的类型支持

在使用 Vuex 时,可以为 state、getters、mutations 和 actions 定义类型,确保类型安全。

// store.ts
import { createStore } from 'vuex';

interface State {
  counter: number;
}

const store = createStore<State>({
  state: {
    counter: 0,
  },
  mutations: {
    increment(state) {
      state.counter++;
    },
  },
  getters: {
    getCount(state): number {
      return state.counter;
    },
  },
});

export default store;

5. 使用类型定义的组合函数

自定义组合函数时,明确参数和返回值类型,可以提高代码的可读性和可维护性。

import { ref } from 'vue';

function useCounter(initialValue: number = 0) {
  const count = ref<number>(initialValue);

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

  return { count, increment };
}

6. 使用类型推断

在可能的情况下,充分利用 TypeScript 的类型推断,尽量减少显式类型声明,以提高代码的简洁性。

const message = ref('Hello, World!'); // TypeScript 会推断为 string

7. 使用 Vue Router 的类型支持

在使用 Vue Router 时,确保定义路由的类型,以及对路由参数的限制。

import { createRouter, createWebHistory, RouteRecordRaw } from 'vue-router';

const routes: Array<RouteRecordRaw> = [
  {
    path: '/',
    name: 'Home',
    component: () => import('../views/Home.vue'),
  },
  {
    path: '/about',
    name: 'About',
    component: () => import('../views/About.vue'),
  },
];

const router = createRouter({
  history: createWebHistory(),
  routes,
});

export default router;

8. 定义和使用类型接口

在大型应用中,可能会需要定义多个接口,确保通过接口进行类型的重用。

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

const fetchUser = async (id: number): Promise<User> => {
  // API 请求...
};

9.避免 any 类型

尽量避免使用 any 类型,因为它会消除 TypeScript 的类型检查机制。

const messageany = 'Hello'// 尽量避免

10.函数标注类型

通过明确标注函数参数和返回值的类型,TypeScript 可以在编译时检查类型的一致性。这有助于捕捉潜在的错误,避免在运行时发生意外情况

<template>
  <input type="text" @input="handleInput" placeholder="输入一些文字" />
  <input type="text" ref="inputElement" placeholder="输入一些文字" />
  <div ref="myDiv">这是一个 div 元素</div>
  <button @click="increment">点击添加</button>
  <p>计数: {{ count }}</p>
</template>

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

// 为模板引用标注类型
const inputElement = ref<HTMLInputElement | null>(null);

// 声明一个 ref 变量,类型为 HTMLDivElement
const myDiv = ref<HTMLDivElement | null>(null);

// 声明一个响应式变量
const count = ref<number>(0);

// 声明一个响应式变量来存储输入值
const inputValue = ref<string>('');

// 处理输入事件,使用 Event 类型标注 或者默认推导的any(`event` 隐式地标注为 `any` 类型)
// 没有类型标注时,这个 `event` 参数会隐式地标注为 `any` 类型。这也会在 `tsconfig.json` 中配置了 `"strict": true` 或 `"noImplicitAny": true` 时报出一个 TS 错误 建议使用断言
const handleInput = (event: Event): void => {
  const target = event.target as HTMLInputElement; // 类型断言
  inputValue.value = target.value; // 获取输入框的值
};

// 函数类型标注 无返回值
const increment = (): void => {
  count.value++;
};

// 定义一个可选参数的函数 返回string类型
const greet = (name?: string, greeting: string = '你好'): string => {
  return `${greeting}, ${name}!`;
};

// 回调函数类型定义
type Callback = (msg: string) => void;

const handleClick = (): void => {
  executeCallback((message) => {
    console.log(message);
  });
};

const executeCallback = (callback: Callback): void => {
  callback('按钮被点击了!');
};

// 定义一个泛型函数,去除数组中的重复元素
const uniqueArray = <T>(arr: T[]): T[] => {
  return Array.from(new Set(arr));
};

// 示例数组
const numbers = [1, 2, 2, 3, 4, 4, 5];
const strings = ['apple', 'banana', 'apple', 'orange'];

// 使用泛型函数
const uniqueNumbers = ref<number[]>(uniqueArray(numbers));
const uniqueStrings = ref<string[]>(uniqueArray(strings));

// 定义一个泛型函数,用于合并两个对象
const mergeObjects = <T, U>(obj1: T, obj2: U): T & U => {
  return { ...obj1, ...obj2 }; // 使用展开运算符合并对象
};

// 示例对象
const objectA = { name: '张三', age: 30 };
const objectB = { hobby: '阅读', location: '北京' };

// 使用泛型函数合并对象 ref中的类型可以单独定义个 type 或者 interface
const mergedObject = ref<{ name: string; age: number; hobby: string; location: string }>(
  mergeObjects(objectA, objectB)
);
</script>

11. provide / inject 标注类型

provide 和 inject 通常会在不同的组件中运行。要正确地为注入的值标记类型,Vue 提供了一个 InjectionKey 接口,它是一个继承自 Symbol 的泛型类型,可以用来在提供者和消费者之间同步注入值的类型

import { provide, inject } from 'vue'  
import type { InjectionKey } from 'vue'  
  
const key = Symbol() as InjectionKey<string>  
  
provide(key, '123'// 若提供的是非字符串值会导致错误  
  
const test = inject(key) // test 的类型:string | undefined
// 当使用字符串注入 key 时,注入值的类型是 `unknown`,需要通过泛型参数显式声明
const test = inject<string>('key'// 类型:string | undefined

12.组件模板引用标注类型

在 Vue 3 中使用 TypeScript 时,为组件模板引用(即使用 ref 获取组件实例)标注类型是一个好习惯。这将帮助我们在使用组件实例时获得更好的类型检查和代码提示。

<template>
  <div>
    <p>我是子组件</p>
  </div>
</template>

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

// 定义子组件的 props(可选)
const props = defineProps<{ message: string }>();
</script>
<template>
  <div>
    <MyChild ref="childRef" message="Hello from parent!" />
    <button @click="callChildMethod">调用子组件的方法</button>
  </div>
</template>

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

// 为子组件实例引用标注类型
const childRef = ref<InstanceType<typeof MyChild> | null>(null);

// 定义调用子组件的方法
const callChildMethod = (): void => {
  if (childRef.value) {
    // 调用子组件的方法,假设子组件有一个名为 showMessage 的方法
    // childRef.value.showMessage(); // 如果子组件有此方法,则可以调用
    console.log('子组件已被引用');
  }
};
</script>

13. 进行类型检查

利用 TypeScript 的静态类型检查,确保编译时捕获错误,提高代码的可靠性。(结合eslint+prettier约束代码规范将得到更好的体验 jsdoc的支持函数的注释调用时候即可得到完整的解释)

14. 参考官方文档

根据项目需要,不断查阅 Vue 3 和 TypeScript 的官方文档,了解最新的功能和最佳实践(多总结多实验多模仿)。