3. TypeScript 基础(下):联合类型、交叉类型、类型别名、函数类型定义、泛型入门
好,咱们接着往下聊。上一节我们把 TypeScript 的基础类型和接口过了一遍,这一节要啃的这几个概念,说实话,才是真正让你从「写 JS」切换到「写 TS」的分水岭。我个人习惯把这一节叫做「类型体操入门课」——学会了这些,你写出来的代码会自带一种「稳」的感觉。
3.1 联合类型:要么这个,要么那个
联合类型用 | 符号表示。说白了就是:「这个变量,可以是类型 A,也可以是类型 B」。
let id: string | number;
id = "abc123"; // 没问题
id = 456; // 也没问题
id = true; // 报错!boolean 不在联合里
我在项目中遇到过最典型的场景:后端返回的用户 ID,有时候是字符串,有时候是数字。你如果只写 string,那数字进来就崩了。联合类型就是用来处理这种「模棱两可」的情况。
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,交叉完直接没法用。
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 就行,清晰又省事。
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 函数就能搞定所有。你想想看,如果没有泛型,你得为每个接口写一个独立的请求函数,那得多累。
<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 的基础就算打牢了。下一节我们会深入泛型的更多用法,包括泛型类、泛型工具类型等。到时候你会发现,原来类型系统可以这么灵活。