3. TypeScript 基础(下):联合类型、交叉类型、类型别名、函数类型定义、泛型入门

好,咱们接着往下聊。上一节我们把 TypeScript 的基础类型和接口过了一遍,这一节要啃的这几个概念,说实话,才是真正让你从「写 JS」切换到「写 TS」的分水岭。我个人习惯把这一节叫做「类型体操入门课」——学会了这些,你写出来的代码会自带一种「稳」的感觉。

3.1 联合类型:要么这个,要么那个

联合类型用 | 符号表示。说白了就是:「这个变量,可以是类型 A,也可以是类型 B」。

let id: string | number;
id = "abc123";   // 没问题
id = 456;        // 也没问题
id = true;       // 报错!boolean 不在联合里

我在项目中遇到过最典型的场景:后端返回的用户 ID,有时候是字符串,有时候是数字。你如果只写 string,那数字进来就崩了。联合类型就是用来处理这种「模棱两可」的情况。

小技巧:用联合类型时,TypeScript 会帮你做类型收窄(Narrowing)。比如你用 typeof 判断一下,TS 就知道当前分支下具体是哪个类型了。

3.2 交叉类型:既要这个,也要那个

交叉类型用 & 符号。它跟联合正好相反——联合是「或」,交叉是「且」。

interface Person {
  name: string;
  age: number;
}

interface Employee {
  company: string;
  salary: number;
}

type Staff = Person & Employee;

const worker: Staff = {
  name: "张三",
  age: 28,
  company: "字节跳动",
  salary: 30000
};

嗯,这里要注意:交叉类型不是简单的「合并属性」。如果两个类型有同名但类型不同的属性,结果会是 never。我曾经踩过这个坑——两个接口里都有 id 字段,一个 string 一个 number,交叉完直接没法用。

避坑指南:交叉类型遇到同名冲突属性时,会取两者的交集。如果类型不兼容(比如 string & number),结果就是 never。写代码时一定要留意属性名冲突。

3.3 类型别名:给你的类型起个名字

type 关键字。说白了就是给一个复杂的类型起个简短的名字,方便复用。

type Point = {
  x: number;
  y: number;
};

type ID = string | number;

function drawPoint(p: Point) {
  console.log(p.x, p.y);
}

我个人习惯把项目中频繁用到的联合类型、对象类型都用 type 提取出来。比如后端 API 返回的「分页参数」类型,我通常会写成:

type Pagination = {
  page: number;
  pageSize: number;
  total: number;
};

这样每个用到分页的函数,参数类型直接写 Pagination 就行,清晰又省事。

核心区别:type 和 interface 都能定义对象类型。但 type 能做联合、交叉、元组等更灵活的操作;interface 可以重复声明合并。我的建议是:定义对象用 interface,定义联合/交叉用 type。

3.4 函数类型定义:把函数也「类型化」

函数在 JS 里是一等公民,在 TS 里也得有类型。你可以直接给参数和返回值加类型:

function add(a: number, b: number): number {
  return a + b;
}

也可以用箭头函数的形式:

const multiply: (x: number, y: number) => number = (x, y) => x * y;

你想想看,如果函数参数是个回调,类型定义就更有用了:

function fetchData(url: string, callback: (data: any) => void) {
  // 模拟请求
  const result = { id: 1, name: "test" };
  callback(result);
}

这里 (data: any) => void 表示「接受一个参数,不返回值」的函数。我在写 React 组件时,经常用这种模式定义事件处理函数的类型。

经验之谈:函数类型定义里,参数名可以省略,但类型不能省。比如 (string, number) => boolean 也是合法的,但可读性差,我一般都会写上参数名。

3.5 泛型入门:让类型也「参数化」

泛型是 TypeScript 里最强大的特性之一。说白了就是:写代码的时候不确定具体类型,让调用者来指定。

举个例子,你想写一个「取数组第一个元素」的函数:

function first<T>(arr: T[]): T | undefined {
  return arr[0];
}

const num = first([1, 2, 3]);       // num 类型是 number | undefined
const str = first(["a", "b"]);      // str 类型是 string | undefined

这里的 <T> 就是泛型参数。它像是一个「类型占位符」,调用时 TypeScript 会自动推断出具体的类型。

我在项目中用过最经典的泛型场景,就是封装 API 请求函数:

async function request<T>(url: string): Promise<T> {
  const res = await fetch(url);
  return res.json();
}

// 使用时指定返回类型
interface User {
  id: number;
  name: string;
}

const user = await request<User>("/api/user/1");
// user 的类型自动变成 User

这样写的好处是:每个接口返回的数据结构都不一样,但一个 request 函数就能搞定所有。你想想看,如果没有泛型,你得为每个接口写一个独立的请求函数,那得多累。

泛型约束:有时候你想限制泛型的范围,可以用 extends 关键字。比如 <T extends { length: number }> 表示 T 必须有 length 属性。这样既能保持灵活性,又能保证安全。

3.6 综合实战:把这些概念串起来

光说不练假把式。咱们来看一个真实项目中会遇到的场景:封装一个「状态管理」的工具函数。

// 定义状态类型
type Status<T> = 
  | { type: "idle" }
  | { type: "loading" }
  | { type: "success"; data: T }
  | { type: "error"; error: string };

// 创建状态的工厂函数
function createStatus<T>(): Status<T> {
  return { type: "idle" };
}

// 使用示例
type UserData = {
  id: number;
  name: string;
};

const userStatus = createStatus<UserData>();
// userStatus 的类型是 Status<UserData>
// 你可以安全地根据 type 字段做类型收窄

这个例子把联合类型、泛型、类型别名全用上了。我在写 React 应用时,这种模式几乎每个页面都会用到——管理异步请求的 loading、成功、失败状态。

注意:泛型不是万能的。如果一个函数内部需要调用类型的某个方法(比如 .length),记得加约束。否则 TypeScript 会报错,因为它不知道 T 有没有这个方法。

好了,这一节的内容就到这儿。联合类型、交叉类型、类型别名、函数类型定义、泛型——这几个概念你掌握了,TypeScript 的基础就算打牢了。下一节我们会深入泛型的更多用法,包括泛型类、泛型工具类型等。到时候你会发现,原来类型系统可以这么灵活。