作为一个 Vue 写前端、Java 写后端的开发者,TypeScript 对我来说是"最熟悉的陌生人"。前端代码量一大,纯 JavaScript 的"动态类型自由"就会变成重构时的噩梦;而 TypeScript 恰好填补了这块空白。真正深入学习后我才发现,它的类型系统远不止"给 JS 加类型"这么简单——它本身几乎是一门独立的类型编程语言。
结构化类型:与 Java 最根本的区别
Java 是名义类型(nominal typing)——类型是否兼容取决于显式的继承与实现关系;TypeScript 是结构化类型(structural typing)——只要结构一致就兼容。这是两者最本质的差异:
interface Point {
x: number;
y: number;
}
// 不需要 implements,结构一致即可
const p = { x: 1, y: 2 };
function draw(p: Point) {}
draw(p); // ✅ 编译通过
对从 Java 转来的开发者,这意味着接口不再是"契约声明",而是"形状描述"。好处是灵活、天然贴合 JavaScript 的对象模型;代价是类型检查只存在于编译期,运行时没有任何类型信息。
类型推断与字面量类型
TypeScript 的类型推断比 Java 的 var 强得多,甚至能推断出"字面量类型":
const name = "Cookie"; // 类型是 "Cookie"(字面量类型)
let count = 0; // 类型是 number
const flags = ["a", "b"] as const; // 类型是 readonly ["a", "b"]
字面量类型配合联合类型,可以把"可选值"约束死,实现类似枚举的穷举校验:
type Role = "admin" | "user" | "guest";
function canDelete(role: Role) {
return role === "admin"; // 传入其他字符串会直接报错
}
泛型:比 Java 更灵活
Java 泛型是"擦除式"的,运行时拿不到类型参数;TypeScript 的泛型在编译期保留完整结构,还能参与计算:
function identity<T>(arg: T): T {
return arg;
}
// 泛型约束:限制 T 必须满足某个形状
function getLength<T extends { length: number }>(arg: T): number {
return arg.length;
}
getLength("hello"); // ✅ string 有 length
getLength([1, 2]); // ✅ array 有 length
类型体操:条件类型与工具类型
这是 TypeScript 最有意思的部分。通过映射类型和内置工具类型,可以"从已有类型推导出全新类型",后端接口定义因此可以大幅复用:
interface User {
id: number;
name: string;
email: string;
password: string;
}
// 内置工具类型:只读、挑选、可选
type ReadonlyUser = Readonly<User>;
type PublicUser = Omit<User, "password">; // 对外永远不暴露密码
type PartialUser = Partial<User>; // 更新接口时全部字段可选
再进一步,条件类型配合 infer 可以"拆解"类型——比如提取函数的返回类型:
type MyReturnType<T> = T extends (...args: any[]) => infer R ? R : never;
function fetchUser() {
return Promise.resolve({ id: 1, name: "Cookie" });
}
type T = MyReturnType<typeof fetchUser>;
// T = Promise<{ id: number; name: string }>
看到这里你应该明白为什么社区叫它"类型体操"了——类型系统本身就像一门图灵完备的小语言,可以在编译期完成大量计算。
类型守卫与收窄
前端要处理大量"来源不可信"的数据(接口返回、localStorage、事件对象),unknown 和可辨识联合(discriminated union)是两大法宝:
type Shape =
| { kind: "circle"; radius: number }
| { kind: "square"; side: number };
function area(s: Shape): number {
switch (s.kind) { // 根据 kind 字段收窄类型
case "circle":
return Math.PI * s.radius ** 2;
case "square":
return s.side * s.side;
}
}
自定义类型守卫可以把"运行时判断"升级成"类型收窄",让编译器和代码阅读者都受益:
function isString(v: unknown): v is string {
return typeof v === "string";
}
function log(v: unknown) {
if (isString(v)) {
console.log(v.toUpperCase()); // 此处 v 已被收窄为 string
}
}
Vue 3 + TypeScript 工程实践
Vue 3 的组合式 API 配合泛型,让组件 props 有了完整的类型提示和默认值支持:
<script setup lang="ts">
interface Props {
title: string;
count?: number;
}
const props = withDefaults(defineProps<Props>(), {
count: 0,
});
const doubled = computed(() => props.count * 2);
const count = ref(0); // 自动推断为 Ref<number>
</script>
响应式 API 的类型几乎全部由推导完成,ref / computed / watch 基本不需要手写类型标注。真正需要花心思的,是把业务实体类型集中管理、跨组件共享。
工程最佳实践
- 开启 strict 模式,把
any当成"红线",用no-explicit-any的 lint 规则治理滥用 - 业务实体类型集中在
types/目录,接口返回类型、组件 props、store 状态共用一份 - 用类型收窄替代运行时防御代码,把数据校验放在边界处(API 层),内部代码保持类型干净
- 泛型组件 + 工具类型,把重复的"结构变换"固化成类型,而不是复制粘贴
总结
把 TypeScript 当成"类型编程语言"而不是"带类型的 JS",才能真正发挥它的价值。对于写惯了 Java 的开发者,TypeScript 反而提供了比 Java 更灵活的类型表达能力——它把"编译期就能发现错误"这个能力带给了前端,这正是前端工程化走向成熟的关键一步。
TypeScript 的价值不在于类型本身,而在于它把错误挡在编译期、让重构变得安全。当你从一个 10 万行、零类型的 JavaScript 项目迁移过来,就会明白这种"安全感"有多珍贵。