TypeScript+Vue 实战:告别 any 滥用,统一接口 / Props / 表单类型,实现类型安全|编码语法规范篇

【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 EventMouseEvent
接口返回 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,如 UserInfoFormConfig
  • 类型别名: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 可以记一下的规则

  1. 优先 interface,需要联合、交叉等再考虑 type
  2. 能用 interface 就别用 type 定义对象
  3. 导出用 export typeimport type,方便做 Tree Shaking
  4. 尽量不写 any,实在要绕过用 unknown
  5. Props、表单、接口数据结构分清楚,各用各的类型

[⬆ 返回目录](#⬆ 返回目录)

八、小结

这一篇主要讲了四块:

  1. 接口设计 :用 interface 描述接口、列表、配置,注意可选、必选和命名约定
  2. Props 类型defineProps<Props>() + withDefaults,不要直接改 props
  3. 表单类型:表单类型单独定义,允许空值,提交前再做转换和校验
  4. 告别 any :用 unknown、泛型、类型守卫,让 TS 真正发挥作用

类型规范不是追求「写法炫酷」,而是减少运行时报错、提高可维护性。

建议从一个小模块开始,先把接口、Props、表单类型统一好,再慢慢扩展到整个项目。

如果这篇文章对你有帮助,欢迎点赞、收藏,有疑问也可以在评论区留言讨论。

[⬆ 返回目录](#⬆ 返回目录)

🔍 系列模块导航

📝 编码语法规范

《一、JS/TS 编码规范实战:Vue 场景变量 / 函数 / 类型标注避坑|编码语法规范篇》
《二、async/await 规范:错误处理 / 避免嵌套 / 防重复请求,异步代码更优雅|编码语法规范篇》
《三、前端 utils 工具函数规范:拆分 / 命名 / 复用全指南,避开全局污染等高频坑|编码语法规范篇》
《四、前端 console 日志规范实战:高效调试 / 垃圾 log 清理与线上安全避坑|编码语法规范篇》
《五、JS 函数单一职责实战:拆分逻辑 / 告别面条代码,写出可维护团队级代码|编码语法规范篇》

《六、TypeScript+Vue 实战:告别 any 滥用,统一接口 / Props / 表单类型,实现类型安全|编码语法规范篇》

👉 跟着系列慢慢学,把技术功底扎扎实实地打牢~

📚 系列总览

前端规范实战系列目前正在持续更新中,当该系列完结之后我会整理出一篇《前端规范实战系列全系列目录导航》,届时会附上文章简介以及跳转链接,方便同学们按顺序体系化的学习~

更新中,敬请期待~

[⬆ 返回目录](#⬆ 返回目录)


技术成长,从来不是比谁写得快,而是比谁写得稳、规范、可维护

哪怕每次只吃透一条规范,长期下来,差距会非常明显。

后续我会持续更新前端规范、工程化、可维护代码相关实战干货,帮你告别面条代码、维护噩梦,在开发与面试中更有底气。

觉得有用欢迎 点赞 + 收藏 + 关注,不错过每一篇实战内容。

我是 Eugene,与你一起写规范、写优质代码,我们下篇干货见~

相关推荐
liuyao_xianhui1 小时前
优选算法_模拟_提莫攻击_C++
开发语言·c++·算法·动态规划·哈希算法·散列表
我是永恒2 小时前
上架一个跨境工具导航网站
前端
.select.2 小时前
c++ 移动赋值/移动构造函数
开发语言·c++
电子羊2 小时前
Spec 编程工作流文档
前端
GISer_Jing2 小时前
从CLI到GUI桌面应用——前端工程化进阶之路
前端·人工智能·aigc·交互
我是鶸2 小时前
secml-malware python library 源码分析及实践
开发语言·python
setmoon2142 小时前
C++代码规范化工具
开发语言·c++·算法
不想看见4042 小时前
C++/Qt 代码规范指南
开发语言·qt
always_TT2 小时前
字符串输入:gets vs fgets(安全问题)
数据库·安全