TypeScript速通笔记(下)

文章来源声明: 原文作者:进击的明明; 来源站点:掘金; 原文链接:https://juejin.cn/post/7691248453423153188; 本文基于上述来源整理/加工,觅优补充点评,仅供技术学习交流。版权归原作者所有。
觅优短评

内容覆盖TS进阶核心,示例直白,适合有基础的开发者快速查漏补缺,尤其适合React项目接入与类型声明配置实践。

前言 --

欢迎回来!上一篇我们把 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)再鼠标左键点击,就能直接跳进去查看这些声明的内容。

目前几乎所有常用的第三方库都有对应的类型声明文件,主要有两种存在形式:

  1. 库自己自带的声明文件
  2. 由 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 提供的类型声明主要分成两大部分:

  1. react、react-dom、node 的类型
    这部分让你在写组件、使用 JSX、操作 DOM、使用 Node 环境相关 API 时,都能获得完整的类型提示和检查。

  2. 图片、样式等模块的类型
    这部分非常实用。它允许你在代码中直接导入图片、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!