【TypeScript + Vue】前端开发:从 any 滥用到类型安全,统一接口/Props/表单类型写法,避开类型混乱与运行时报错坑!

📑 文章目录
- 一、前言:为什么要写这篇
- 二、先搞清楚:类型到底是什么?
- [2.1 类型不是「玄学」,是「约束」](#2.1 类型不是「玄学」,是「约束」)
- [2.2 any 为什么是「万恶之源」](#2.2 any 为什么是「万恶之源」)
- 三、核心规范一:接口(Interface)怎么设计
- [3.1 用 Interface 定义「数据结构」](#3.1 用 Interface 定义「数据结构」)
- [3.2 可选、必选、只读](#3.2 可选、必选、只读)
- [3.3 常见踩坑:后端字段和前端不一致](#3.3 常见踩坑:后端字段和前端不一致)
- [四、核心规范二:Vue Props 类型怎么写](#四、核心规范二:Vue Props 类型怎么写)
- [4.1 用 Interface 定义 Props](#4.1 用 Interface 定义 Props)
- [4.2 有默认值的 Props](#4.2 有默认值的 Props)
- [4.3 Props 常见坑](#4.3 Props 常见坑)
- 五、核心规范三:表单类型统一
- [5.1 表单值和接口数据分开](#5.1 表单值和接口数据分开)
- [5.2 完整示例:新增/编辑表单](#5.2 完整示例:新增/编辑表单)
- [5.3 表单校验类型(如 Element Plus)](#5.3 表单校验类型(如 Element Plus))
- [六、核心规范四:从 any 过渡到类型安全](#六、核心规范四:从 any 过渡到类型安全)
- [6.1 替代 any 的几种写法](#6.1 替代 any 的几种写法)
- [6.2 unknown 的正确用法](#6.2 unknown 的正确用法)
- [6.3 泛型:让类型跟着数据走](#6.3 泛型:让类型跟着数据走)
- 七、编码规范速查
- [7.1 命名](#7.1 命名)
- [7.2 组织方式](#7.2 组织方式)
- [7.3 可以记一下的规则](#7.3 可以记一下的规则)
- 八、小结
- [🔍 系列模块导航](#🔍 系列模块导航)
同学们好,我是 Eugene(尤金),一名多年中后台前端开发工程师。
(Eugene 发音 /juːˈdʒiːn/,大家怎么顺口怎么叫就好)
很多前端开发者都会遇到一个瓶颈:
代码能跑,但不够规范;功能能实现,但维护起来特别痛苦;一个人写没问题,一到团队协作就各种混乱、踩坑、返工。
想写出干净、优雅、可维护 的专业代码,靠的不是天赋,而是体系化的规范 + 真实实战经验。
这一系列《前端规范实战》,我会用大白话 + 真实业务场景,不讲玄学、不堆理论,只分享能直接落地的规范、标准与避坑指南。
帮你从「会写代码」真正升级为「会写优质、可维护、团队级别的代码」。
TS 类型规范:从 any 到类型安全,接口/Props/表单类型统一|编码语法规范篇
一、前言:为什么要写这篇
日常写 Vue 时,很多人都在用 TS,但类型往往是「能用就行」:
要么到处 any,要么类型和实际数据对不上。
这篇不讲太底层的原理,重点放在:日常该怎么写、为什么这么写、容易踩的坑在哪,适合:
- 会写 JS,但对 TS 类型有点懵的同学
- 从零学 TS 的初学者
- 想巩固类型基础、顺便改掉坏习惯的熟手
结构上会按:基础概念 → 实战规范 → 常见坑 来写,尽量用完整示例 + 文字说明讲清楚。
[⬆ 返回目录](#⬆ 返回目录)
二、先搞清楚:类型到底是什么?
2.1 类型不是「玄学」,是「约束」
类型可以理解成:给变量、函数、对象加一层「说明书」,告诉你:
- 这个变量能放什么
- 函数要传什么、会返回什么
- 对象里有哪些字段、分别是什么类型
有了类型,TS 就能在写代码时帮你发现错误,而不是等到运行时才报错。
[⬆ 返回目录](#⬆ 返回目录)
2.2 any 为什么是「万恶之源」
ts
// ❌ any 写法:编译器完全不管了
let user: any = { name: '张三' };
user.xxx.yyy.zzz; // 编译通过!但运行时报错
user(); // 编译通过!但 user 根本不是函数
一旦用了 any,TS 就放弃检查,等于把 TS 的优势全部丢掉。
规范第一条:尽量不用 any,如果暂时不知道类型,用 unknown 再断言或收窄。
[⬆ 返回目录](#⬆ 返回目录)
三、核心规范一:接口(Interface)怎么设计
3.1 用 Interface 定义「数据结构」
接口用来描述对象的结构,最常用的场景就是:接口数据、列表项、配置对象。
ts
// ✅ 推荐:用 interface 描述接口返回的数据
interface UserInfo {
id: number;
name: string;
avatar?: string; // 可选字段用 ?
createdAt: string;
}
// 使用
const user: UserInfo = {
id: 1,
name: '张三',
createdAt: '2025-03-20'
// avatar 可以不写,因为是可选
};
[⬆ 返回目录](#⬆ 返回目录)
3.2 可选、必选、只读
ts
interface FormConfig {
required: boolean; // 必填
maxLength?: number; // 可选
readonly id: string; // 只读,创建后不能再改
}
[⬆ 返回目录](#⬆ 返回目录)
3.3 常见踩坑:后端字段和前端不一致
后端经常是 user_name,前端习惯 userName,可以这样约定:
ts
// 方案一:接口类型按后端字段来,转一层再用
interface UserApiResponse {
user_id: number;
user_name: string;
created_at: string;
}
// 转换函数 + 前端使用的类型
interface User {
userId: number;
userName: string;
createdAt: string;
}
function transformUser(apiUser: UserApiResponse): User {
return {
userId: apiUser.user_id,
userName: apiUser.user_name,
createdAt: apiUser.created_at
};
}
// 方案二:保持和后端一致,前端直接用
interface User {
user_id: number;
user_name: string;
created_at: string;
}
建议:接口类型和接口字段一一对应 ,避免 userId / user_id 混用导致类型和实际不符。
[⬆ 返回目录](#⬆ 返回目录)
四、核心规范二:Vue Props 类型怎么写
4.1 用 Interface 定义 Props
html
<script setup lang="ts">
// 1. 先定义 Props 的接口
interface Props {
title: string;
count?: number; // 可选,有默认值
list: string[];
user?: UserInfo; // 复杂对象用已有接口
}
// 2. 用 defineProps 并指定泛型
const props = defineProps<Props>();
// 3. 使用时都有类型提示
console.log(props.title); // string
console.log(props.count); // number | undefined
</script>
<template>
<div>{{ props.title }}</div>
</template>
[⬆ 返回目录](#⬆ 返回目录)
4.2 有默认值的 Props
ts
// 方式一:withDefaults + 泛型
interface Props {
title: string;
count?: number;
}
const props = withDefaults(defineProps<Props>(), {
count: 0 // 默认值
});
// 方式二:分开写(Vue 3.3+ 推荐)
interface Props {
title: string;
count?: number;
}
const props = withDefaults(defineProps<Props>(), {
count: () => 0 // 对象/数组用工厂函数
});
[⬆ 返回目录](#⬆ 返回目录)
4.3 Props 常见坑
坑 1:必传的没传,类型却没报错
html
// ❌ 子组件要求必传 user,但父组件没传,可能只在运行时才发现
interface Props {
user: UserInfo; // 必传
}
// ✅ 父组件使用时要确保传入
<ChildComponent :user="currentUser" />
坑 2:直接改 props
ts
// ❌ 不要直接改 props
props.count = 10; // 报错,props 是只读的
// ✅ 需要修改时,用本地状态
const localCount = ref(props.count ?? 0);
[⬆ 返回目录](#⬆ 返回目录)
五、核心规范三:表单类型统一
5.1 表单值和接口数据分开
表单里的值(如空字符串、未选状态)和接口期望的类型往往不一样,要单独定义表单类型。
ts
// 接口/列表用的类型
interface User {
id: number;
name: string;
age: number;
department: string;
}
// 表单类型:所有字段可选,空值用 '' 或 undefined
interface UserForm {
name: string;
age: number | ''; // 输入框可能是空字符串
department: string;
}
// 或者用 Partial + 扩展
type UserFormInput = {
[K in keyof User]?: User[K] | ''; // 每个字段都可能是 ''
};
[⬆ 返回目录](#⬆ 返回目录)
5.2 完整示例:新增/编辑表单
html
<script setup lang="ts">
import { ref, reactive } from 'vue';
// 1. 列表/接口用的类型
interface User {
id: number;
name: string;
age: number;
department: string;
}
// 2. 表单类型:和接口对应,但允许空
interface UserForm {
name: string;
age: number | '';
department: string;
}
// 3. 初始表单
const form = reactive<UserForm>({
name: '',
age: '',
department: ''
});
// 4. 编辑时:接口数据 -> 表单
function loadForEdit(user: User) {
form.name = user.name;
form.age = user.age;
form.department = user.department;
}
// 5. 提交前:表单 -> 接口数据
function submit(): User | null {
if (!form.name || form.age === '' || !form.department) {
return null;
}
return {
id: 0, // 新增时
name: form.name,
age: Number(form.age),
department: form.department
};
}
</script>
<template>
<form @submit.prevent="submit">
<input v-model="form.name" placeholder="姓名" />
<input v-model.number="form.age" type="number" placeholder="年龄" />
<select v-model="form.department">
<option value="">请选择</option>
<option value="技术部">技术部</option>
</select>
</form>
</template>
要点:
- 表单类型单独定义,允许
''、undefined - 提交前做校验,再转成接口需要的类型
[⬆ 返回目录](#⬆ 返回目录)
5.3 表单校验类型(如 Element Plus)
ts
// Form 实例类型
import type { FormInstance, FormRules } from 'element-plus';
const formRef = ref<FormInstance>();
const rules: FormRules<UserForm> = {
name: [{ required: true, message: '请输入姓名', trigger: 'blur' }],
age: [{ required: true, message: '请输入年龄', trigger: 'blur' }],
department: [{ required: true, message: '请选择部门', trigger: 'change' }]
};
async function handleSubmit() {
if (!formRef.value) return;
await formRef.value.validate();
const data = submit(); // 上面的 submit
if (data) {
// 调用接口
}
}
[⬆ 返回目录](#⬆ 返回目录)
六、核心规范四:从 any 过渡到类型安全
6.1 替代 any 的几种写法
| 场景 | 不要用 | 推荐用 |
|---|---|---|
| 暂时不知道类型 | any |
unknown + 类型收窄 |
| 随便什么对象 | any |
Record<string, unknown> |
| 事件对象 | any |
Event、MouseEvent 等 |
| 接口返回 | any |
定义接口类型 |
[⬆ 返回目录](#⬆ 返回目录)
6.2 unknown 的正确用法
ts
// unknown 必须收窄后才能用
function handleData(data: unknown) {
// ❌ data.xxx 报错:unknown 不能直接访问属性
// ✅ 类型收窄
if (typeof data === 'object' && data !== null && 'name' in data) {
console.log((data as { name: string }).name);
}
// ✅ 用类型守卫
if (isUser(data)) {
console.log(data.name); // 安全
}
}
function isUser(val: unknown): val is User {
return (
typeof val === 'object' &&
val !== null &&
'id' in val &&
'name' in val
);
}
[⬆ 返回目录](#⬆ 返回目录)
6.3 泛型:让类型跟着数据走
ts
// 通用的列表请求
interface ApiResponse<T> {
code: number;
data: T;
message: string;
}
// 使用
const res = await fetchUserList();
// res.data 会被推断为 User[]
const users: User[] = res.data;
[⬆ 返回目录](#⬆ 返回目录)
七、编码规范速查
7.1 命名
- 接口:
PascalCase,如UserInfo、FormConfig - 类型别名:
PascalCase,如UserForm - 泛型:单字母或
TUser等形式
[⬆ 返回目录](#⬆ 返回目录)
7.2 组织方式
ts
// 1. 简单项目:类型和组件放一起
// 2. 中大型项目:抽到 types/ 目录
// types/user.ts
export interface User { ... }
export interface UserForm { ... }
// 组件里
import type { User, UserForm } from '@/types/user';
[⬆ 返回目录](#⬆ 返回目录)
7.3 可以记一下的规则
- 优先
interface,需要联合、交叉等再考虑type - 能用
interface就别用type定义对象 - 导出用
export type或import type,方便做 Tree Shaking - 尽量不写
any,实在要绕过用unknown - Props、表单、接口数据结构分清楚,各用各的类型
[⬆ 返回目录](#⬆ 返回目录)
八、小结
这一篇主要讲了四块:
- 接口设计 :用
interface描述接口、列表、配置,注意可选、必选和命名约定 - Props 类型 :
defineProps<Props>()+withDefaults,不要直接改 props - 表单类型:表单类型单独定义,允许空值,提交前再做转换和校验
- 告别 any :用
unknown、泛型、类型守卫,让 TS 真正发挥作用
类型规范不是追求「写法炫酷」,而是减少运行时报错、提高可维护性。
建议从一个小模块开始,先把接口、Props、表单类型统一好,再慢慢扩展到整个项目。
如果这篇文章对你有帮助,欢迎点赞、收藏,有疑问也可以在评论区留言讨论。
[⬆ 返回目录](#⬆ 返回目录)
🔍 系列模块导航
📝 编码语法规范
《一、JS/TS 编码规范实战:Vue 场景变量 / 函数 / 类型标注避坑|编码语法规范篇》
《二、async/await 规范:错误处理 / 避免嵌套 / 防重复请求,异步代码更优雅|编码语法规范篇》
《三、前端 utils 工具函数规范:拆分 / 命名 / 复用全指南,避开全局污染等高频坑|编码语法规范篇》
《四、前端 console 日志规范实战:高效调试 / 垃圾 log 清理与线上安全避坑|编码语法规范篇》
《五、JS 函数单一职责实战:拆分逻辑 / 告别面条代码,写出可维护团队级代码|编码语法规范篇》
《六、TypeScript+Vue 实战:告别 any 滥用,统一接口 / Props / 表单类型,实现类型安全|编码语法规范篇》
👉 跟着系列慢慢学,把技术功底扎扎实实地打牢~
📚 系列总览
前端规范实战系列目前正在持续更新中,当该系列完结之后我会整理出一篇《前端规范实战系列全系列目录导航》,届时会附上文章简介以及跳转链接,方便同学们按顺序体系化的学习~
更新中,敬请期待~
[⬆ 返回目录](#⬆ 返回目录)
技术成长,从来不是比谁写得快,而是比谁写得稳、规范、可维护。
哪怕每次只吃透一条规范,长期下来,差距会非常明显。
后续我会持续更新前端规范、工程化、可维护代码相关实战干货,帮你告别面条代码、维护噩梦,在开发与面试中更有底气。
觉得有用欢迎 点赞 + 收藏 + 关注,不错过每一篇实战内容。
我是 Eugene,与你一起写规范、写优质代码,我们下篇干货见~