前言
欢迎回来!上一篇我们把 TypeScript 的基础类型、函数、枚举、断言这些"入门必会"的东西过了一遍。今天继续往下挖,进入更实用、也更有意思的进阶部分:交叉类型、泛型、索引签名、映射类型、类型声明文件,以及在 React 里怎么优雅地用 TS。
同样的原则:尽量大白话,例子多一点,让你看完就能上手。准备好了吗?我们直接开干。
一、交叉类型:把多个类型"揉"在一起
交叉类型的语法非常简单:
ts
type xxx = interface1 & interface2
它的作用类似于接口继承,用来把多个类型组合成一个新类型,最常用于对象类型。
你可以把它想象成:把两个(或多个)类型的"属性清单"合并在一起,新类型必须同时满足所有被交叉的类型的要求。
举个例子:
ts
interface Person {
name: string;
}
interface Employee {
employeeId: number;
}
type Staff = Person & Employee;
// Staff 现在同时拥有 name 和 employeeId
const staff: Staff = {
name: "张三",
employeeId: 1001
};
和接口继承的对比
交叉类型和接口继承都能实现对象类型的组合,但它们处理同名属性冲突的方式不一样。
- 接口继承时,如果子接口和父接口有同名属性,类型必须兼容,否则直接报错。
- 交叉类型则是把同名属性的类型再做一次交叉。如果两个属性类型完全不兼容(比如一个是
string,一个是number),最终结果会变成never,赋值时就会出问题。
简单记:想"严格继承并检查冲突"用接口继承;想"灵活合并,冲突自己处理"用交叉类型。两者各有适用场景。
二、泛型:让类型也能当参数传
泛型是 TypeScript 里最强大、也最值得掌握的特性之一。它的核心思想是:
在保证类型安全的前提下,让函数、接口、类能和多种类型一起工作,从而实现真正的复用。
最简单的例子
ts
function id<T>(value: T): T {
return value;
}
在函数名后面加一个尖括号 <>,里面写一个类型变量(这里是 T)。这相当于把"类型"也当成参数传进去了。
调用时:
ts
let num = id(10); // TS 自动推断 T 是 number
let str = id("hello"); // 自动推断 T 是 string
大多数情况下你可以直接传实参,省略 <类型>,TypeScript 会根据传入的值自动推断。只有在推断不准确或无法推断时,才需要手动写:
ts
let num = id<number>(10);
泛型约束:给 T 划个范围
有时候你希望在函数内部访问某个属性,比如 value.length。如果直接写:
ts
function id<T>(value: T): T {
console.log(value.length); // 报错!T 可能是 number,没有 length
return value;
}
因为 T 可以是任意类型,TypeScript 不知道它一定有 length。这时就需要泛型约束 ,缩小 T 能指代的类型范围。
常见的约束方式有几种:
1. 指定更具体的类型
ts
function id<T>(value: T[]): T[] {
console.log(value.length); // 现在安全了,因为是数组
return value;
}
把类型改成 T[],就明确告诉 TypeScript "这一定是数组",length 就能放心用了。
2. 用 extends 添加约束
ts
interface HasLength {
length: number;
}
function id<T extends HasLength>(value: T): T {
console.log(value.length); // 安全
return value;
}
T extends HasLength 的意思是:T 必须满足 HasLength 这个接口的要求(至少要有 length 属性)。这样既保留了灵活性,又保证了安全性。
3. 多个泛型变量
一个函数里可以有多个类型变量:
ts
function pair<T, U>(first: T, second: U): [T, U] {
return [first, second];
}
调用时 TypeScript 会分别推断 T 和 U。
4. 泛型接口
在接口名称后面加 <类型>,就变成了泛型接口。使用时需要显式指定具体类型:
ts
interface Result<T> {
data: T;
success: boolean;
}
const res: Result<string> = {
data: "成功",
success: true
};
5. 数组其实也是泛型接口
你平时写的 number[] 和 Array<number>,本质上就是 TypeScript 内置的泛型接口。所以数组天生就支持泛型。
6. 泛型类
类同样可以配合泛型使用。定义类时在类名后面加 <类型>,这个类型变量就能在类的内部使用:
ts
class Box<T> {
value: T;
constructor(value: T) {
this.value = value;
}
}
const numberBox = new Box<number>(123);
const stringBox = new Box<string>("hello");
创建实例时需要在类名后面加上 <类型> 来明确指定。
泛型工具类型:TypeScript 内置的"快捷方式"
TypeScript 内置了不少常用的工具类型,用来简化日常操作。这里重点介绍几个最常用的:
(1)Partial
把一个类型的所有属性变成可选:
ts
interface Props {
id: string;
children: number[];
}
type PartialProps = Partial<Props>;
// 等价于 { id?: string; children?: number[] }
注意:Partial 会返回一个新类型 ,不会修改原来的 Props。
(2)Readonly
把指定类型中的所有属性变成只读。用法和 Partial 一样,这里就不重复展开了。
(3)Pick<Type, Keys>
从某个类型中挑选一组属性,构造出新类型:
ts
interface Props {
id: string;
title: string;
children: number[];
}
type PickProps = Pick<Props, "id" | "title">;
// 结果只有 id 和 title 两个属性
非常适合从大接口里"挑出需要的部分"。
(4)Record<Keys, Type>
用来快速构建一个对象类型。Keys 表示有哪些键,Type 表示这些键对应的值类型:
ts
type RecordObj = Record<"a" | "b" | "c", string[]>;
// 等价于
// type RecordObj = {
// a: string[];
// b: string[];
// c: string[];
// }
写配置对象、字典类型时特别方便。
三、索引签名类型与映射类型
索引签名类型
核心作用:描述"键名未知,但值的类型已知"的对象。
比如你想表示一个对象,键可以是任意字符串,值必须是数字:
ts
type StringToNumber = {
[key: string]: number;
};
这在处理动态键名的场景(比如配置项、缓存对象)时非常有用。
映射类型:key in 和 keyof
映射类型可以理解为"批量生成属性"。
keyof 表示"某个类型中的所有键"。结合 in,我们就能写出很强大的类型变换。
假设我们有一个类型 Props,想定义一个新类型,键名和 Props 完全一样,但值全部变成 number:
ts
type Props = {
name: string;
age: number;
};
type NumberProps = {
[key in keyof Props]: number;
};
// 结果:{ name: number; age: number }
这就是映射类型的基本用法。
手写一个 Partial
有了映射类型,我们甚至可以自己手写实现 Partial:
ts
type MyPartial<T> = {
[P in keyof T]?: T[P];
};
和内置的 Partial 效果一样。
索引查询类型(Indexed Access Types)
可以用类似"查字典"的方式,取出某个属性的类型:
ts
type Props = {
a: number;
b: string;
c: boolean;
};
type TypeA = Props["a"]; // number
type TypeABC = Props["a" | "b" | "c"]; // number | string | boolean
这种写法在需要精确提取属性类型时特别方便。
四、类型声明文件:让 JS 也能被 TS 理解
TypeScript 为 JavaScript 运行时可用的所有内置 API 都提供了内置类型声明文件。你只需要按住 Ctrl(Mac 上是 Command)再鼠标左键点击,就能直接跳进去查看这些声明的内容。
目前几乎所有常用的第三方库都有对应的类型声明文件,主要有两种存在形式:
- 库自己自带的声明文件
- 由 DefinitelyTyped(社区维护的类型仓库)提供
在 package.json 里通常会指定这个包需要加载哪个类型声明文件。TypeScript 官方文档也提供了专门的页面,帮我们快速查询 DefinitelyTyped 里有没有对应的类型声明,不用自己去 GitHub 大海捞针了。
创建自己的类型声明文件
主要有两个常见场景:
1. 项目内共享类型
把公共的类型定义抽到单独的 .d.ts 文件里,方便整个项目复用。
2. 为已有 JS 文件提供类型声明
在把 JS 项目迁移到 TS 时,经常需要给已有的 .js 文件补类型。做法是:
- 新建一个同名的
.d.ts文件(比如utils.js对应utils.d.ts) - 在里面用
declare关键字描述该 JS 文件导出的函数、变量、类等的类型签名 - 不要写任何实现代码,只写类型
然后在 tsconfig.json 里确保:
allowJs: true- 或者通过
typeRoots/paths正确指向声明文件
这样 TypeScript 在引用这个 JS 模块时,就能读取到对应的类型信息,实现类型检查和智能提示。
五、在 React 中使用 TypeScript
创建支持 TypeScript 的 React 项目最简单的方式,依然是用 Create React App:
bash
npx create-react-app 项目名称 --template typescript
如果项目已经建好,也可以手动安装相关依赖:
bash
npm i --save typescript @types/node @types/react @types/react-dom @types/jest
装好之后,把文件后缀从 .js / .jsx 改成 .ts / .tsx,就可以开始享受类型带来的智能提示和错误检查了。
但真正让 React + TypeScript 项目"开箱即用"的,还有一个非常关键、却常被忽略的文件------react-app-env.d.ts。
react-app-env.d.ts:React 项目默认的类型声明文件
用 CRA 创建 TypeScript 项目后,你会在项目根目录(或 src 目录下)看到一个名为 react-app-env.d.ts 的文件。它就是 React 项目默认的类型声明文件。
这个文件通常只有一行核心内容:
ts
/// <reference types="react-scripts" />
这就是传说中的三斜线指令。
三斜线指令是什么?
三斜线指令(Triple-Slash Directive)是 TypeScript 用来声明依赖关系的特殊注释。它的作用是告诉编译器:"我需要加载某个类型声明文件包"。
其中 types 属性表示依赖的类型声明文件包的名称。上面这行代码的意思是:
告诉 TypeScript:请帮我加载
react-scripts这个包提供的类型声明。
react-scripts 的类型声明文件包含了什么?
react-scripts 提供的类型声明主要分成两大部分:
-
react、react-dom、node 的类型
这部分让你在写组件、使用 JSX、操作 DOM、使用 Node 环境相关 API 时,都能获得完整的类型提示和检查。
-
图片、样式等模块的类型
这部分非常实用。它允许你在代码中直接导入图片、SVG、CSS 等文件,而不会报"找不到模块"或"没有类型声明"的错误。例如:
tsimport logo from './logo.svg'; import styles from './App.module.css';没有这些声明的话,TypeScript 会认为这些导入是非法的。
TypeScript 是怎么自动加载这个文件的?
TypeScript 会自动加载项目中的 .d.ts 文件,从而提供类型声明。你可以自己验证这一点:去修改 tsconfig.json 里的 include 配置,把 react-app-env.d.ts 排除掉,再重新打开项目,你会发现图片导入、部分全局类型提示立刻消失。再加回来,又恢复正常。
这说明 CRA 已经帮你把最基础的类型环境配置好了,你几乎不需要手动去写这些底层声明。
在 React 项目里使用 TypeScript,除了创建项目、安装依赖、改后缀名之外,真正让类型系统"跑起来"的关键角色,就是这个不起眼的 react-app-env.d.ts 文件和它里面的三斜线指令。它默默地帮你加载了 react、react-dom、node 以及资源文件的类型声明,让你能安心写组件、安心导入图片和样式。
理解了这一点,你再看到项目根目录里那个 .d.ts 文件,就不会觉得它是多余的了。
六、小结
这一篇我们把 TypeScript 进阶部分的核心内容过了一遍:
- 交叉类型:灵活组合多个类型
- 泛型:让类型也能当参数,实现真正的复用
- 泛型约束、多个泛型、泛型接口、泛型类
- 内置工具类型 Partial、Readonly、Pick、Record
- 索引签名、映射类型、索引查询类型
- 类型声明文件的使用与创建
- 在 React 中快速接入 TypeScript
希望这篇文章能帮助你更好地书写TS!