Vue3和TypeScript一起写项目真的香吗?会不会增加很多额外工作量?
Vue2用惯了,加上TS会不会是给自己找罪受?直到接了个团队协作的电商后台项目,硬着头皮用Vue3 Composition API+TS重构了两个模块,才彻底打消顾虑——它的“香”是能摸得着的代码爽感和效率提升,至于额外工作量,只要前期搭好架子、用对工具,后期维护起来反而能省一半心。
为什么说Vue3+TS是绝配,不是硬凑?
很多人觉得Vue2是渐进式框架,加JSX都有点重,更别说TS这种静态类型语言了,但Vue3从诞生那天起,TS就是核心支持项,甚至源码都是用TS写的,根本不是后期加的补丁,适配度拉满的点有这些:
Props、Emit等核心API自带类型推导,不用写一堆重复注释
Vue2里想给组件传值,得先写data或者props选项,然后在或者methods里凭记忆用,怕传错还得加一堆JSDoc注释,* @type {Array<{id: number; name: string; price: number}>} /,一旦数据结构变了,注释忘改,后面接手的人就得踩坑。
Vue3+TS的话,直接用defineProps、defineEmits配合TypeScript语法就能搞定,连IDE的提示都丝滑到飞起,举个电商后台常见的商品列表组件例子:
// 定义商品的类型接口
interface ProductItem {
id: number;
name: string;
price: number;
stock: number;
isOnSale: boolean;
createTime: string;
}
// 子组件的props,支持必填、可选、默认值、类型校验
const props = defineProps<{
productList: ProductItem[];: string; // 可选的标题
showStockAlert?: boolean; // 是否显示库存预警,默认false
// 如果有复杂的默认值,不能直接在<>里写,得用withDefaults
}>();
// withDefaults处理复杂默认值
const props = withDefaults(defineProps<{
productList: ProductItem[];: string;
showStockAlert?: boolean;
}>(), { '全部商品',
showStockAlert: true,
});
// 定义子组件的emit,支持事件名、参数类型校验
const emit = defineEmits<{
// 第一个参数是事件名,后面是参数数组的类型
'add-to-cart': [productId: number];
'view-detail': [productId: number];
// 可以写不带参数的事件
'refresh-list': [];
}>();
写好这些之后,父组件传productList的时候,如果数组里缺了id或者price,IDE立刻会给红线警告;子组件调用emit的时候,事件名写错或者参数类型不对也不行,比如写成emit('add-cart', '123'),直接就报错——根本不用等到浏览器控制台看,写代码的时候就能发现问题,调试效率至少提升30%。
Composition API的ref、reactive配合TS,类型推导更精准
Vue3的Composition API本身就是为了解决Vue2 Options API逻辑分散、复用困难的问题,加上TS之后,逻辑复用的同时类型也能一起复用,简直是如虎添翼。
先说ref:Vue2里ref('123')和ref(123)都是ref对象,访问的时候得用.value,但IDE没法自动推导.value的类型,除非加as或者JSDoc,Vue3+TS里,如果初始值是明确的,ref会自动推导类型:
// 自动推导为Ref<number>
const count = ref(0);
// 自动推导为Ref<string | null>,因为初始值有null
const searchKeyword = ref<string | null>(null);
// 如果要给初始类型加更复杂的约束,比如只能是'asc'或'desc'
const sortOrder = ref<'asc' | 'desc'>('asc');
如果初始值是对象或者数组,用ref也可以,但更推荐用reactive,不过reactive有个坑:不能直接替换整个对象,否则类型会丢失,这个时候可以结合readonly或者toRefs来处理,但TS本身也能帮你注意到这个问题:
const productState = reactive<ProductItem>({
id: 1,
name: 'MacBook Pro',
price: 12999,
stock: 50,
isOnSale: true,
createTime: '2024-05-20',
});
// ❌ 直接替换整个对象,TS会报错(虽然JS运行时不会,但逻辑上是错的)
// productState = { id: 2, ... };
// ✅ 用Object.assign或者逐个修改属性
Object.assign(productState, { id: 2, name: 'iPad Air' });
再说逻辑复用:比如电商后台里很多地方都要做分页,我们可以抽一个usePagination的 composable:
// 定义分页的类型接口
interface PaginationParams {
page: number;
pageSize: number;
}
interface PaginationResult<T> {
list: T[];
total: number;
page: number;
pageSize: number;
setPage: (page: number) => void;
setPageSize: (pageSize: number) => void;
reset: () => void;
}
// 定义usePagination composable,支持传入默认的page和pageSize
export function usePagination<T>(defaultParams?: Partial<PaginationParams>): PaginationResult<T> {
const defaultPage = defaultParams?.page || 1;
const defaultPageSize = defaultParams?.pageSize || 10;
const page = ref(defaultPage);
const pageSize = ref(defaultPageSize);
const list = ref<T[]>([]);
const total = ref(0);
const setPage = (newPage: number) => {
if (newPage >= 1) page.value = newPage;
};
const setPageSize = (newPageSize: number) => {
if (newPageSize >= 1) {
pageSize.value = newPageSize;
page.value = 1; // 修改每页条数时重置到第一页
}
};
const reset = () => {
page.value = defaultPage;
pageSize.value = defaultPageSize;
};
return {
list,
total,
page,
pageSize,
setPage,
setPageSize,
reset,
};
}
用的时候直接传类型参数就行,比如商品列表:
const { list: productList, total, page, pageSize, setPage, setPageSize, reset } = usePagination<ProductItem>({ pageSize: 20 });
这样传进去的list.value就是ProductItem[]类型,不是any,访问list.value[0].price的时候也有提示,完全不用担心踩空。
路由、状态管理这些生态工具也完全适配TS
Vue3的官方生态工具Vue Router 4和Pinia(Vuex的替代品)都是用TS写的,适配起来特别简单,不需要装额外的适配器。
先说Pinia:以前用Vuex 3的时候,加TS得写一堆Module、State、Getter、Action的类型声明,非常麻烦,Pinia就不一样了,直接用defineStore配合TS就行:
import { defineStore } from 'pinia';
import type { ProductItem } from '@/types/product';
export const useCartStore = defineStore('cart', {
state: () => ({
// 自动推导类型为{productId: number; quantity: number}[]
cartItems: [] as { productId: number; quantity: number }[],
}),
getters: {
// 自动推导返回值类型
totalPrice: (state) => {
// 这里可以再结合useProductStore获取商品的详细信息,计算总价
// 不过为了演示,先随便写
return state.cartItems.reduce((sum, item) => sum + item.quantity * 100, 0);
},
},
actions: {
// 自动推导参数类型
addToCart(productId: number) {
const existingItem = this.cartItems.find(item => item.productId === productId);
if (existingItem) {
existingItem.quantity++;
} else {
this.cartItems.push({ productId, quantity: 1 });
}
},
removeFromCart(productId: number) {
this.cartItems = this.cartItems.filter(item => item.productId !== productId);
},
},
});
在组件里用的时候,IDE也能提示state、getters、actions的名字,非常方便。
再说Vue Router 4:可以给路由的meta字段加类型约束,比如有些路由需要登录,有些是管理员才能访问:
// 先定义meta的类型
import 'vue-router';
declare module 'vue-router' {
interface RouteMeta {
requiresAuth?: boolean; // 是否需要登录
requiresAdmin?: boolean; // 是否需要管理员权限
}
}
// 然后在路由配置里用
const routes = [
{
path: '/',
name: 'home',
component: () => import('@/views/HomeView.vue'),
meta: { requiresAuth: false },
},
{
path: '/admin',
name: 'admin',
component: () => import('@/views/AdminView.vue'),
meta: { requiresAuth: true, requiresAdmin: true },
},
];
这样在路由守卫里访问meta.requiresAuth或者meta.requiresAdmin的时候,IDE也有提示,不会写错字段名。
说“增加额外工作量”的人,可能是前期搭架子没搭好
不可否认,刚上手Vue3+TS的时候,确实会比纯Vue2+JS多写一点代码,比如定义接口、类型约束这些,但这些都是“前期投入,后期回报”的事情,如果前期搭好架子、用对工具,后期维护起来反而能省很多事,那怎么才能减少前期的额外工作量呢?
直接用官方推荐的脚手架工具
别自己从零搭Webpack或者Vite配置了,直接用npm create vue@latest,这个是Vue官方的脚手架工具,能一键生成带TS、ESLint、Prettier、Pinia、Vue Router的项目,配置都是官方优化过的,不需要再自己改。
创建的时候选择这些选项就行:
- 项目名称:随便填
- TypeScript:Yes
- JSX Support:根据自己的需求选(如果喜欢用JSX就选Yes,不然No)
- Vue Router:Yes
- Pinia:Yes
- Vitest:根据需求选(单元测试工具)
- Cypress:根据需求选(端到端测试工具)
- ESLint:Yes
- Prettier:Yes
这样创建出来的项目,已经把TS的配置(比如tsconfig.json、env.d.ts)、ESLint的规则、Prettier的格式都配置好了,直接就能写代码。
提前定义好全局的类型文件
在src目录下创建一个types文件夹,用来存放全局的类型接口,比如商品类型、用户类型、订单类型这些,这样所有的组件、composable、store都能引用,不用重复定义。
比如src/types/user.ts:
export interface UserInfo {
id: number;
username: string;
avatar: string;
role: 'user' | 'admin';
createTime: string;
}
src/types/order.ts:
import type { ProductItem } from './product';
import type { UserInfo } from './user';
export interface OrderItem {
product: ProductItem;
quantity: number;
}
export interface Order {
id: number;
orderNo: string;
user: UserInfo;
items: OrderItem[];
totalPrice: number;
status: 'pending' | 'paid' | 'shipped' | 'delivered' | 'cancelled';
createTime: string;
payTime?: string;
shipTime?: string;
deliverTime?: string;
}
善用自动生成工具
很多时候我们不需要自己手写接口,可以用自动生成工具,比如根据后端的API文档(Swagger/OpenAPI)自动生成TypeScript类型接口和API请求函数,这样能省很多时间。
比如常用的工具有:
- swagger-typescript-api
- openapi-typescript-codegen
- axios-typescript(这个是封装axios的,也可以结合自动生成工具用)
举个swagger-typescript-api的例子,只需要一行命令就能根据后端的Swagger JSON文件生成类型和API函数:
npx swagger-typescript-api -p http://localhost:3000/swagger.json -o ./src/api -n api.ts --axios
生成的api.ts文件里会包含所有的接口类型和API请求函数,直接就能用,非常方便。
用any或者unknown要谨慎,但也别完全不用
有些刚上手TS的人,为了避免类型错误,所有的地方都用any,这样就失去了TS的意义;还有些人,为了追求“完美的类型安全”,写了一堆复杂的类型声明,反而增加了很多额外工作量。
正确的做法是:用any或者unknown要谨慎,但也别完全不用,比如刚开始重构项目的时候,可以先用unknown或者any过渡,然后慢慢把类型补全;比如第三方库的类型声明有问题,或者后端返回的数据结构不确定,可以先用any或者unknown,然后再根据实际情况处理。
什么样的项目适合用Vue3+TS?
不是所有的项目都适合用Vue3+TS,比如个人的小Demo、只有一两个人维护的小项目,纯Vue3+JS可能更灵活,更快;但如果是团队协作的中大型项目、需要长期维护的项目、对代码质量要求比较高的项目,Vue3+TS绝对是首选,因为它能:
- 提前发现代码错误,减少线上bug
- 提高代码的可读性和可维护性
- 促进团队协作,避免接手别人代码的时候踩坑
- 更好地适配Vue3的生态工具
最后说说我的真实体验
之前我用Vue2+JS写了一个电商后台项目,有3个开发人员,维护了1年多,后来发现代码越来越乱,很多地方的类型都不确定,每次修改功能都要花很多时间调试,接手的新人也总是踩坑,后来我用Vue3+TS重构了这个项目,前期搭架子加定义类型花了大概1周的时间,重构第一个模块花了大概3天,后面的模块越来越快,最后整个项目重构完只花了1个月的时间,重构之后,线上bug减少了80%左右,团队协作的效率也提高了很多,新人接手项目的时候,看类型接口就能知道数据结构,根本不用问我。
所以说,Vue3+TS真的香,前期的额外工作量是值得的,后期维护起来能省很多心,如果你还没试过Vue3+TS,不妨用官方的脚手架工具创建一个项目试试,相信你会爱上它的。
版权声明
本文仅代表作者观点,不代表Code前端网立场。
本文系作者Code前端网发表,如需转载,请注明页面地址。
code前端网

