TypeScript 学习笔记:从类型系统到 Vue 工程实践

作为一个 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 项目迁移过来,就会明白这种"安全感"有多珍贵。