TypeScript速通笔记(下)

前言

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

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

  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:

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 提供的类型声明主要分成两大部分:

  1. react、react-dom、node 的类型

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

  2. 图片、样式等模块的类型

    这部分非常实用。它允许你在代码中直接导入图片、SVG、CSS 等文件,而不会报"找不到模块"或"没有类型声明"的错误。例如:

    ts 复制代码
    import 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!

相关推荐
lzhdim1 小时前
C#不为人知的10个魔法特性:资深开发者也会震惊的底层奥秘
java·前端·javascript·算法·c#
zach2 小时前
前端落地实战:Next.js 项目 Nginx + PM2 生产部署全流程(附踩坑总结)
前端·pm2·next.js
overmind2 小时前
oeasy教h5前端001 修改的乐趣_直接修改在线网页
前端
只睡四小时2 小时前
JS 手写 D-pad 空间导航:电视端焦点引擎实战
android·开发语言·前端·javascript·ecmascript·hls·空间导航
恋猫de小郭2 小时前
Android 原生的 Compose A2UI 也来了,你还抱着 XML 养老吗?
android·前端·flutter
庄园特聘拆椅狂魔2 小时前
从连连看到粒子消散——前端小游戏与视觉特效的技术选型
前端
JudithHuang2 小时前
React 常用 Hooks
前端·react.js·前端框架
sycmancia3 小时前
Qml——Window元素使用
前端
每天都好困4 小时前
03 AR 投影与误差修正:把地图上的点贴到视频画面上
前端