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