Vue3中JS和TS差在哪些方面(一份后端程序员的对照笔记)
作者:Nontee22
前言
作为一个写惯了 Java 的人,学 Vue3 时最别扭的不是响应式 API,而是"这个变量到底要不要写类型"这件事本身。JS 世界里变量随手一声明就用,TS 又要求处处标注——一开始我只觉得啰嗦,直到把两种写法逐个对照着敲了一遍才看明白:TS 并没有改变 Vue3 的核心逻辑,它只是在关键位置加上了"类型说明书"。
这份笔记就是把"说明书加在哪、为什么加在这些位置"整理成对照。所有代码基于 <script setup> 语法,JS 和 TS 两版并排放,代码都是我实际敲过的最小示例。
先说一个总的前提,能帮 Java 同行省很多困惑:TS 的类型只活在编译期。编译成 JS 之后,所有类型标注全部消失,运行期没有任何类型检查。这个事实会影响后面好几处的理解。
一、基础变量:冒号后面的事,Java 人秒懂
JS 里变量可以随时改类型,TS 里变量声明了类型就锁死:
// 【纯 JS 写法】 let username = "张三" let age = 25 let hobbies = ["阅读", "游泳"]
// 【Vue3 TS 写法】 let username: string = "张三" let age: number = 25 let hobbies: string[] = ["阅读", "游泳"] // 差异解析:在变量名后加冒号和类型。数组可以在类型后加 []。
let username: string 在 Java 人眼里就是 String username 的换装出场,没有任何理解成本。数组标注 string[] 对应 List<String>,也顺。
但接下来这个就不太一样了——
// 【纯 JS 写法】
let userInfo = { name: "李四", age: 30 }
let status = "loading" // 后续可能会变成 "success" 或 "error"
// 【Vue3 TS 写法】
let userInfo: { name: string; age: number } = { name: "李四", age: 30 }
let status: "loading" | "success" | "error" = "loading"
// 差异解析:TS 可以限制对象的具体结构,也可以用 | 限制变量只能是某几个特定字符串之一。
status 这行的联合类型让我停下来看了一会儿。这个"变量只能是这几个字符串之一"的需求,Java 里的标准答案是写一个 enum Status { LOADING, SUCCESS, ERROR }——TS 用 | 一行搞定,连类型都不用单独定义,可以直接内联在变量声明和函数签名里。写惯了 Java 的会怀念 enum 的方法承载能力,但不得不承认前端这种"状态就三个值"的场景,字面量联合类型轻快得多。
二、ref / reactive:泛型的既视感
响应式 API 这边,TS 的介入方式很值得玩味——它只在"编译器猜不出来"的地方出手。
1. ref 包装数据
// 【纯 JS 写法】
import { ref } from 'vue'
const count = ref(0)
const userList = ref([])
const currentProduct = ref(null)
userList.value.push("张三")
currentProduct.value = { id: 1, title: "手机" }
// 【Vue3 TS 写法】
import { ref } from 'vue'
const count = ref(0) // 基础类型 TS 会自动推断,无需改动
const userList = ref<string[]>([]) // 空数组必须用尖括号 < > 告诉 TS 里面装什么
const currentProduct = ref<{ id: number; title: string } | null>(null)
userList.value.push("张三") // 正确
// userList.value.push(123) // 报错:类型不匹配
currentProduct.value = { id: 1, title: "手机" }
// 差异解析:当 ref 初始值为空数组或 null 时,TS 无法猜测你未来要存什么,必须手动加泛型 < >。
ref<string[]>([]) 这一行,Java 程序员会有强烈的既视感——而且这不是"像"泛型,这就是泛型:ref 本身是个泛型函数,尖括号里的 string[] 是类型实参,和 new ArrayList<String>() 里传给容器的类型参数是同一个机制。容器刚创建时是空的,元素类型必须由人显式声明,编译器没有从空数组里推断出任何东西的依据。
ref(0) 不用标类型这点也顺理成章:初始值就是 0,推断出 number,跟 Java 的 var 局部变量推断是一个思路。
2. reactive 包装对象
// 【纯 JS 写法】
import { reactive } from 'vue'
const form = reactive({
username: "",
password: ""
})
form.username = "admin"
// 【Vue3 TS 写法】
import { reactive } from 'vue'
const form = reactive({
username: "",
password: ""
})
form.username = "admin"
// 差异解析:reactive 传入的是具体对象,TS 会根据初始值自动推断类型,所以写法和 JS 完全一样,无需额外加类型。
reactive 一节代码完全没变——因为传入的是具体对象而不是空容器,TS 从初始值的结构推断出了 { username: string; password: string }。这一节给我的启发反倒是关于"什么时候不需要写类型":能被推断的就别写。TS 的类型标注是给编译器的信息,信息已经有了还重复一遍,就是噪声。
三、Props 与 Emits:编译期 vs 运行期的分水岭
这是 Vue3 里 JS 和 TS 写法差异最大的地方,也是我个人觉得 TS 最值的地方。
1. defineProps(父传子)
// 【纯 JS 写法】
// 使用运行时声明,通过构造函数 (String, Number) 来限制
const props = defineProps({
title: String,
count: {
type: Number,
default: 0
}
})
// 【Vue3 TS 写法】
// 使用基于类型的声明,配合 withDefaults 设置默认值
const props = withDefaults(defineProps<{
title?: string
count?: number
}>(), {
title: '默认标题',
count: 0
})
// 差异解析:JS 用大写的 String/Number,TS 用小写的 string/number。TS 写法能提供更好的代码自动补全。
先看语法本身:defineProps<{...}>() 的尖括号是在给编译宏传类型实参——和上一节 ref<T> 是同一个泛型指纹。而两种写法的类型检查发生在完全不同的时刻:JS 的运行时声明是编译后的代码里真实存在的一段校验逻辑,组件跑起来才拦;TS 的类型声明在编译期就把不匹配拦下了。
那 TS 写法编译成 JS 后,props 校验是不是就没了?我最初是这么担心的——毕竟开头说了类型全部消失。后来查了 Vue 编译器的行为才发现:SFC 编译器会把类型声明翻译回等价的运行时声明(简单的 string/number 没问题;复杂到编译器无法翻译的类型才需要回退到原始的运行时声明)。两种写法殊途同归,区别只是校验发生得更早、编辑器提示更好。
对后端来说这个对比很熟悉:运行时声明像 Bean Validation,请求进来才抛 MethodArgumentNotValidException;类型声明像方法签名,编译期就过不了。TS 让前端组件的接口也拥有了"编译期契约"。
2. defineEmits(子传父)
// 【纯 JS 写法】
// 只声明事件名称,不限制参数
const emit = defineEmits(['change', 'update'])
function handleSave() {
emit('change', 101)
emit('update', '新数据', 1690000000)
}
// 【Vue3 TS 写法】
// 声明事件名称,同时严格限制每个事件能传递的参数类型和数量
const emit = defineEmits<{
change: [id: number]
update: [value: string, timestamp: number]
}>()
function handleSave() {
emit('change', 101) // 正确
// emit('change', '字符串') // 报错:类型必须是 number
}
// 差异解析:JS 只管事件名,TS 连事件携带的参数类型和个数都管,避免了子组件乱传参数。
TS 版的 defineEmits 读起来就是一个事件接口定义:事件名当方法名,元组当参数列表。组件对外发布的每个事件都有了签名,父组件监听时编辑器能提示参数类型——这在多人协作的大组件树里价值远超单打独斗。后端的类比:JS 版像没有 OpenAPI 文档的接口,TS 版像带完整 schema 的接口定义。
四、事件处理:断言这一刀
1. 普通点击事件
// 【纯 JS 写法】
function handleButtonClick(e) {
console.log("点击坐标:", e.clientX, e.clientY)
}
// 【Vue3 TS 写法】
function handleButtonClick(e: MouseEvent) {
console.log("点击坐标:", e.clientX, e.clientY)
}
// 差异解析:必须给参数 e 加上 MouseEvent 类型,否则 TS 不认识 e.clientX。
2. 输入框事件(新手最常遇到的报错)
// 【纯 JS 写法】
function handleInput(e) {
// JS 中可以直接访问 e.target.value
console.log("输入的值是:", e.target.value)
}
// 【Vue3 TS 写法】
function handleInput(e: Event) {
// TS 中 e.target 默认可能是任何元素,不一定有 value 属性
// 必须使用 as 进行"类型断言",明确告诉 TS 这是一个输入框
const target = e.target as HTMLInputElement
console.log("输入的值是:", target.value)
}
// 差异解析:TS 非常严谨,它不知道触发事件的具体是哪个 HTML 标签,所以需要你手动"断言"它是 HTMLInputElement。
e.target as HTMLInputElement 这一刀,形式上就是 Java 的强制类型转换 (HTMLInputElement) e.target。但语义上有本质差异,这是我这次对照里印象最深的一处:
Java 的强转在运行时真的会检查,转错了抛 ClassCastException,程序当场死给你看。TS 的断言纯粹是编译期的口头保证——编译之后这段代码里根本没有类型信息,运行时不会做任何检查。断言错了不会立刻报错,而是炸在后续某个访问属性的地方,报错位置离出错原因可能隔了好几层调用。
所以 Java 同行要把"强转"的直觉校准一下:TS 的 as 更像是对编译器说"这一步我拿人格担保",担保失败没有任何保险。DOM 事件这里风险低(input 事件的 target 确实几乎总是输入框),但养成"断言前先想想为什么类型系统推断不出来"的习惯没坏处。
五、获取 DOM 元素:?.与 Optional 的隔空握手
JS 里 ref(null) 直接用,TS 必须明确 ref 未来绑到什么标签上:
// 【纯 JS 写法】
import { ref, onMounted } from 'vue'
const inputRef = ref(null)
onMounted(() => {
// JS 中直接调用 focus,不检查 inputRef 是否为空
inputRef.value.focus()
})
// 【Vue3 TS 写法】
import { ref, onMounted } from 'vue'
// 必须加上 | null,因为组件刚初始化时 DOM 还不存在,值确实是 null
const inputRef = ref<HTMLInputElement | null>(null)
onMounted(() => {
// 使用 ?. 可选链操作符,防止 inputRef.value 为 null 时导致程序崩溃
inputRef.value?.focus()
})
// 差异解析:TS 强制你考虑 DOM 还没渲染出来的"空值"情况,使用 <HTMLInputElement | null> 和 ?. 能让代码更健壮。
这一节是 Java 8 之后出生的后端最亲切的:类型里显式写 | null,调用时用 ?.——这就是把 Optional<HTMLInputElement> 的哲学揉进了类型系统。区别在于 Java 的 null 要包一层 Optional 才"可见",TS 把可空性直接编码进类型里,ref<HTMLInputElement | null> 和 ref<HTMLInputElement> 在类型层面是两种东西。谁更彻底见仁见智,但"让 null 出现在签名里"这件事,TS 做得比裸用 Java 更强制。
而 JS 版那行 inputRef.value.focus() 之所以能跑,只是因为 onMounted 的时机恰好保证 DOM 已经渲染。把这段代码挪个时机就是经典的 Cannot read properties of null。TS 版的 ?. 不是防御性编程,是对真实生命周期的诚实。
六、如何平滑过渡
原文的三条建议我原样保留,因为每一条都踩过:
- 先写 JS 逻辑,再补 TS 类型:刚开始不习惯时,按 JS 思维把逻辑写完,然后看着编辑器的红色报错一步步补类型。这个过程和 Java 里让 IDE 画波浪线然后逐个修是一样的节奏。
- 实在不知道什么类型时用
any:let data: any相当于告诉 TS"闭嘴,别管这个变量"。后端可以把它类比成Object加上全关的类型检查——能用,但每用一次就在类型防线上开一个洞,极个别复杂场景再用。 - 多看提示:VS Code 里输入
props.或emit(弹出的提示框就是 TS 在发挥作用。习惯看提示之后,开发效率会反超纯 JS——这部分体验我确实在写这篇笔记的过程中体会到了。
写在最后:几个后端视角的感受
Vue3 的 TS API 有一个统一指纹:把类型当参数传。 回头看这份对照里 TS 出手的位置——ref<T>、defineProps<T>、defineEmits<T>——“类型说明书"几乎全安装在尖括号里,也就是泛型实参的位置。对 Java 人来说这是天大的好消息:给容器和方法标类型参数的肌肉记忆可以整段迁移过来,不用学新东西。反过来说,不属于泛型指纹的两处——联合类型的 | 和断言的 as——才是 TS 真正的"新语法”,需要单独建心智模型。
类型系统像回家,但不是同一个家。 冒号声明、泛型、编译期检查,处处有 Java 的影子,上手成本低到出乎意料。但 TS 是结构化类型(形状匹配就行),Java 是名义类型(必须声明"我实现了")——写法上像 Java,理念上它更像给动态语言打的静态补丁,灵活的地方(联合类型、内联对象类型)比 Java 松,严格的地方(可空性、断言无兜底)又各有取舍。
最值钱的区间是组件边界。 这份对照写下来,我自己的体感是:变量声明加不加类型的日常收益一般,Props/Emits 这类组件之间的契约才是 TS 收益最大的地方——契约在编译期说清楚,联调时少掉的是一整类"传参不匹配"的问题。后端世界里接口文档和 schema 的价值有多大,这里的 TS 价值就有多大。
类型擦除是理解一切差异的钥匙。 开头那句话在结尾再回收一次:TS 的类型只活在编译期。联合类型的运行时无感、断言的无保险、类型声明被编译器翻译回运行时声明——这几点想通了,JS 转 TS 路上大部分"为什么"都有了答案。
对后端来说,Vue3 + TS 上手的真实成本不在语法,在心智模型从"运行时校验"切换到"编译期契约"。这份对照笔记就是那个切换过程的存档。
到此这篇关于Vue3中JS和TS差在哪些方面的文章就介绍到这了,更多相关Vue3中JS和TS区别内容请搜索脚本之家以前的文章或继续浏览下面的相关文章希望大家以后多多支持脚本之家!
