我们已经结束了 TypeScript 类型能力的学习,这一节将进入 TypeScript 的实战应用篇。实战篇主要包括了工程能力、框架集成、ECMAScript 语法、TSConfig 解析以及 Node API 开发这五个部分。
在这一节,我们主要介绍 TypeScript 的工程能力基础,包括类型指令、类型声明、命名空间这么几个部分。这些概念不仅可以帮助你了解到 TypeScript 工程能力的核心理念,也是接下来实战篇内容的前置基础。
要开始学习工程能力,其实我们可以从一个很简单的场景开始。如果你已经有一定 TypeScript 的使用经验,那你很有可能遇到过这么一个场景:出现了莫名其妙的类型报错,但你又不知道从何入手解决,想让 TypeScript 直接忽略掉这一行出错的代码?此时,类型指令就是你最需要的工具。
# 类型检查指令
在前端世界的许多工具中,其实都提供了 行内注释(Inline Comments) 的能力,用于支持在某一处特定代码使用特殊的配置来覆盖掉全局配置。最常见的即是 ESLint 与 Prettier 提供的禁用检查能力,如 /* eslint-disable-next-lint */
、<!-- prettier-ignore -->
等。TypeScript 中同样提供了数个行内注释(这里我们称为类型指令),来进行单行代码或单文件级别的配置能力。这些指令均以 // @ts-
开头 ,我们依次来介绍。
# ts-ignore 与 ts-expect-error
ts-ignore
应该是使用最为广泛的一个类型指令了,它的作用就是直接禁用掉对下一行代码的类型检查:
// @ts-ignore
const name: string = 599;
2
基本上所有的类型报错都可以通过这个指令来解决,但由于它本质是上 ignore 而不是 disable,也就意味着如果下一行代码并没有问题,那使用 ignore 反而就是一个错误了。因此 TypeScript 随后又引入了一个更严格版本的 ignore,即 ts-expect-error
,它只有在下一行代码真的存在错误时才能被使用,否则它会给出一个错误:
// @ts-expect-error
const name: string = 599;
// @ts-expect-error 错误使用此指令,报错
const age: number = 599;
2
3
4
5
在这里第二个 expect-error 指令会给出一个报错:无意义的 expect-error 指令。
那这两个功能相同的指令应该如何取舍?我的建议是在所有地方都不要使用 ts-ignore,直接把这个指令打入冷宫封存起来。原因在上面我们也说了,对于这类 ignore 指令,本来就应当确保下一行真的存在错误时才去使用。
这两个指令只能对单行代码生效,但如果我们有非常多的类型报错要处理(比如正在将一个 JavaScript 文件迁移到 TypeScript),难道要一个个为所有报错的地方都添加上禁用检查指令?当然不,正如 ESLint 中可以使用 /* eslint-disable-next-line */
禁用下一行代码检查,也可以使用 /* eslint-disable */
禁用整个文件检查一样, TypeScript 中也提供了对整个文件生效的类型指令:ts-check
与 ts-nocheck
。
# ts-check 与 ts-nocheck
我们首先来看 ts-nocheck ,你可以把它理解为一个作用于整个文件的 ignore 指令,使用了 ts-nocheck 指令的 TS 文件将不再接受类型检查:
// @ts-nocheck 以下代码均不会抛出错误
const name: string = 599;
const age: number = 'linbudu';
2
3
那么 ts-check
呢?这看起来是一个多余的指令,因为默认情况下 TS 文件不是就会被检查吗?实际上,这两个指令还可以用在 JS 文件中。要明白这一点,首先我们要知道,TypeScript 并不是只能检查 TS 文件,对于 JS 文件它也可以通过类型推导与 JSDoc 的方式进行不完全的类型检查。
// JavaScript 文件
let myAge = 18;
// 使用 JSDoc 标注变量类型
/** @type {string} */
let myName;
class Foo {
prop = 599;
}
2
3
4
5
6
7
8
9
10
在上面的代码中,声明了初始值的 myAge 与 Foo.prop
都能被推导出其类型,而无初始值的 myName 也可以通过 JSDoc 标注的方式来显式地标注类型。
但我们知道 JavaScript 是弱类型语言,表现之一即是变量可以被赋值为与初始值类型不一致的值,比如上面的例子进一步改写:
let myAge = 18;
myAge = "90"; // 与初始值类型不同
/** @type {string} */
let myName;
myName = 599; // 与 JSDoc 标注类型不同
2
3
4
5
6
我们的赋值操作在类型层面显然是不成立的,但我们是在 JavaScript 文件中,因此这里并不会有类型报错。如果希望在 JS 文件中也能享受到类型检查,此时 ts-check
指令就可以登场了:
// @ts-check
/** @type {string} */
const myName = 599; // 报错!
let myAge = 18;
myAge = '200'; // 报错!
2
3
4
5
6
这里我们的 ts-check
指令为 JavaScript 文件也带来了类型检查,而我们同时还可以使用 ts-expect-error
指令来忽略掉单行的代码检查:
// @ts-check
/** @type {string} */
// @ts-expect-error
const myName = 599; // OK
let myAge = 18;
// @ts-expect-error
myAge = '200'; // OK
2
3
4
5
6
7
8
而 ts-nocheck
在 JS 文件中的作用和 TS 文件其实也一致,即禁用掉对当前文件的检查。如果我们希望开启对所有 JavaScript 文件的检查,只是忽略掉其中少数呢?此时我们在 TSConfig 中启用 checkJs
配置,来开启对所有包含的 JS 文件的类型检查,然后使用 ts-nocheck
来忽略掉其中少数的 JS 文件。
除了类型指令以外,在实际项目开发中还有一个你会经常打交道的概念:类型声明。
# 类型声明
在此前我们其实就已经接触到了类型声明,它实际上就是 declare
语法:
declare var f1: () => void;
declare interface Foo {
prop: string;
}
declare function foo(input: Foo): Foo;
declare class Foo {}
2
3
4
5
6
7
8
9
我们可以直接访问这些声明:
declare let otherProp: Foo['prop'];
但不能为这些声明变量赋值:
// × 不允许在环境上下文中使用初始值
declare let result = foo();
// √ Foo
declare let result: ReturnType<typeof foo>;
2
3
4
5
这些类型声明就像我们在 TypeScript 中的类型标注一样,会存放着特定的类型信息,同时由于它们并不具有实际逻辑,我们可以很方便地使用类型声明来进行类型兼容性的比较、工具类型的声明与测试等等。
除了手动书写这些声明文件,更常见的情况是你的 TypeScript 代码在编译后生成声明文件:
// 源代码
const handler = (input: string): boolean => {
return input.length > 5;
}
interface Foo {
name: string;
age: number;
}
const foo: Foo = {
name: "林不渡",
age: 18
}
class FooCls {
prop!: string;
}
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
这段代码在编译后会生成一个 .js
文件和一个 .d.ts
文件,而后者即是类型声明文件:
// 生成的类型定义
declare const handler: (input: string) => boolean;
interface Foo {
name: string;
age: number;
}
declare const foo: Foo;
declare class FooCls {
prop: string;
}
2
3
4
5
6
7
8
9
10
11
12
13
这样一来,如果别的文件或是别的项目导入了这段代码,它们就能够从这些类型声明获得对应部分的类型,这也是类型声明的核心作用:将类型独立于 .js
文件进行存储。别人在使用你的代码时,就能够获得这些额外的类型信息。同时,如果你在使用别人没有携带类型声明的 .js
文件,也可以通过类型声明进行类型补全,我们在后面还会了解更多。
接下来,我们要学习如何通过 TypeScript 类型声明的能力,让项目中的类型覆盖更加完整。
# 让类型定义全面覆盖你的项目
在开始学习下面的内容前,不妨先想想你是否遇到过这么几个场景?
- 想要使用一个 npm 包,但它发布的时间太早,根本没有携带类型定义,于是你的项目里就出现了这么一处没有被类型覆盖的地方。
- 你想要在代码里导入一些非代码文件,反正 Webpack 会帮你处理,但是可恶的 TS 又报错了?
- 这个项目在运行时动态注入了一些全局变量(如
window.errorReporter
),你想要在代码里直接这样访问,却发现类型又报错了...
这些问题都可以通过类型声明来解决,这也是它的核心能力:通过额外的类型声明文件,在核心代码文件以外去提供对类型的进一步补全。类型声明文件,即 .d.ts
结尾的文件,它会自动地被 TS 加载到环境中,实现对应部分代码的类型补全。
声明文件中并不包含实际的代码逻辑,它做的事只有一件:为 TypeScript 类型检查与推导提供额外的类型信息,而使用的语法仍然是 TypeScript 的 declare 关键字,只不过现在我们要进一步学习其它打开方式了。
要详细学习声明文件与 declare 关键字,我们不妨先来看看如何解决上面的问题。首先是无类型定义的 npm 包,我们可以通过 declare module 的方式来提供其类型:
import foo from 'pkg';
const res = foo.handler();
2
3
这里的 pkg 是一个没有类型定义的 npm 包(实际并不存在),我们来看如何为它添加类型提示。
declare module 'pkg' {
const handler: () => boolean;
}
2
3
现在我们的 res 就具有了 boolean 类型!declare module 'pkg'
会为默认导入 foo
添加一个具有 handler 的类型,虽然这里的 pkg
根本不存在。我们也可以在 declare module
中使用默认导出:
declare module 'pkg2' {
const handler: () => boolean;
export default handler;
}
import bar from 'pkg2';
bar();
2
3
4
5
6
7
8
在
'pkg'
的类型声明中,你也可以使用export const
,效果是一致的,但由于对'pkg2'
我们使用了默认导入,因此必须要有一个export default
。
除了为缺失类型的模块声明类型以外,使用类型声明我们还可以为非代码文件,如图片、CSS文件等声明类型。
对于非代码文件,比如说 markdown 文件,假设我们希望导入一个 .md
文件,由于其本质和 npm 包一样是一条导入语句,因此我们可以类似地使用 declare module 语法:
// index.ts
import raw from './note.md';
const content = raw.replace('NOTE', `NOTE${new Date().getDay()}`);
// declare.d.ts
declare module '*.md' {
const raw: string;
export default raw;
}
2
3
4
5
6
7
8
9
10
对于非代码文件的导入,更常见的其实是 .css
、.module.css
、.png
这一类,但基本语法都相似,我们在后面还会见到更多。
总结一下,declare module
通常用于为没有提供类型定义的库进行类型的补全,以及为非代码文件提供基本类型定义。但在实际使用中,如果一个库没有内置类型定义,TypeScript 也会提示你,是否要安装 @types/xxx
这样的包。那这个包又是什么?
# DefinitelyTyped
简单来说,@types/
开头的这一类 npm 包均属于 DefinitelyTyped (opens new window) ,它是 TypeScript 维护的,专用于为社区存在的无类型定义的 JavaScript 库添加类型支持,常见的有 @types/react
@types/lodash
等等。通过 DefinitelyTyped 来提供类型定义的包常见的有几种情况,如 Lodash 这样的库仍然有大量 JavaScript 项目使用,将类型定义内置在里面不一定是所有人都需要的,反而会影响包的体积。还有像 React 这种不是用纯 JavaScript / TypeScript 书写的库,需要自己来手写类型声明(React 是用 Flow 写的,这是一门同样为 JavaScript 添加类型的语言,或者说语法)。
举例来说,只要你安装了 @types/react
,TypeScript 会自动将其加载到环境中(实际上所有 @types/
下的包都会自动被加载),并作为 react 模块内部 API 的声明。但这些类型定义并不一定都是通过 declare module
,我们下面介绍的命名空间 namespace 其实也可以实现一样的能力。
先来看 @types/node
中与 @types/react
中分别是如何进行类型声明的:
// @types/node
declare module 'fs' {
export function readFileSync(/** 省略 */): Buffer;
}
// @types/react
declare namespace React {
function useState<S>(): [S, Dispatch<SetStateAction<S>>];
}
2
3
4
5
6
7
8
9
可以看到,@types/node
中仍然使用 declare module
的方式为 fs
这个内置模块声明了类型,而 @types/react
则使用的是我们没见过的 declare namespace
。别担心,我们会在后面详细介绍。
回到上面的最后一个问题,如果第三方库并不是通过导出来使用,而是直接在全局注入了变量,如 CDN 引入与某些监控埋点 SDK 的引入,我们需要通过 window.xxx
的方式访问,而类型声明很显然并不存在。此时我们仍然可以通过类型声明,但不再是通过 declare module
了。
# 扩展已有的类型定义
对全局变量的声明,还是以 window 为例,实际上我们如果 Ctrl + 点击代码中的 window,会发现它已经有类型声明了:
declare var window: Window & typeof globalThis;
interface Window {
// ...
}
2
3
4
5
这行代码来自于 lib.dom.d.ts
文件,它定义了对浏览器文档对象模型的类型声明,这就是 TypeScript 提供的内置类型,也是“出厂自带”的类型检查能力的依据。类似的,还有内置的 lib.es2021.d.ts
这种文件定义了对 ECMAScript 每个版本的类型声明新增或改动等等。
我们要做的,实际上就是在内置类型声明的基础之上,再新增一部分属性。而别忘了,在 JavaScript 中当你访问全局变量时,是可以直接忽略 window
的:
onerror = () => {};
反过来,在类型声明中,如果我们直接声明一个变量,那就相当于将它声明在了全局空间中:
// 类型声明
declare const errorReporter: (err: any) => void;
// 实际使用
errorReporter("err!");
2
3
4
5
而如果我们就是想将它显式的添加到已有的 Window
接口中呢?在接口一节中我们其实已经了解到,如果你有多个同名接口,那么这些接口实际上是会被合并的,这一特性在类型声明中也是如此。因此,我们再声明一个 Window 接口即可:
interface Window {
userTracker: (...args: any[]) => Promise<void>;
}
window.userTracker("click!")
2
3
4
5
类似的,我们也可以扩展来自 @types/
包的类型定义:
declare module 'fs' {
export function bump(): void;
}
import { bump } from 'fs';
2
3
4
5
总结一下这两个部分,TypeScript 通过 DefinitelyTyped ,也就是 @types/
系列的 npm 包来为无类型定义的 JavaScript npm 包提供类型支持,这些类型定义 的 npm 包内部其实就是数个 .d.ts
这样的声明文件。
而这些声明文件主要通过 declare / namespace 的语法进行类型的描述,我们可以通过项目内额外的声明文件,来实现为非代码文件的导入,或者是全局变量添加上类型声明。而对于多个类型声明文件,如果我们想复用某一个已定义的类型呢?此时三斜线指令就该登场了。
# 三斜线指令
三斜线指令就像是声明文件中的导入语句一样,它的作用就是声明当前的文件依赖的其他类型声明。而这里的“其他类型声明”包括了 TS 内置类型声明(lib.d.ts
)、三方库的类型声明以及你自己提供的类型声明文件等。
三斜线指令本质上就是一个自闭合的 XML 标签,其语法大致如下:
/// <reference path="./other.d.ts" />
/// <reference types="node" />
/// <reference lib="dom" />
2
3
需要注意的是,三斜线指令必须被放置在文件的顶部才能生效。
这里的三条指令作用其实都是声明当前文件依赖的外部类型声明,只不过使用的方式不同:分别使用了 path、types、lib 这三个不同属性,我们来依次解析。
使用 path 的 reference 指令,其 path 属性的值为一个相对路径,指向你项目内的其他声明文件。而在编译时,TS 会沿着 path 指定的路径不断深入寻找,最深的那个没有其他依赖的声明文件会被最先加载。
// @types/node 中的示例
/// <reference path="fs.d.ts" />
2
使用 types 的 reference 指令,其 types 的值是一个包名,也就是你想引入的 @types/
声明,如上面的例子中我们实际上是在声明当前文件对 @types/node
的依赖。而如果你的代码文件(.ts
)中声明了对某一个包的类型导入,那么在编译产生的声明文件(.d.ts
)中会自动包含引用它的指令。
/// <reference types="node" />
使用 lib 的 reference 指令类似于 types,只不过这里 lib 导入的是 TypeScript 内置的类型声明,如下面的例子我们声明了对 lib.dom.d.ts
的依赖:
// vite/client.d.ts
/// <reference lib="dom" />
2
而如果我们使用 /// <reference lib="esnext.promise" />
,那么将依赖的就是 lib.esnext.promise.d.ts
文件。
这三种指令的目的都是引入当前文件所依赖的其他类型声明,只不过适用场景不同而已。
如果说三斜线指令的作用就像导入语句一样,那么命名空间(namespace)就像一个模块文件一样,将一组强相关的逻辑收拢到一个命名空间内部。
# 命名空间
假设一个场景,我们的项目里需要接入多个平台的支付 SDK,最开始只有微信支付和支付宝:
class WeChatPaySDK {}
class ALiPaySDK {}
2
3
然后又多了美团支付、虚拟货币支付(比如 Q 币)、信用卡支付等等:
class WeChatPaySDK {}
class ALiPaySDK {}
class MeiTuanPaySDK {}
class CreditCardPaySDK {}
class QQCoinPaySDK {}
2
3
4
5
6
7
8
9
随着业务的不断发展,项目中可能需要引入越来越多的支付 SDK,甚至还有比特币和以太坊,此时将这些所有的支付都放在一个文件内未免过于杂乱了。这些支付方式其实大致可以分成两种:现实货币与虚拟货币。此时我们就可以使用命名空间来区分这两类 SDK:
export namespace RealCurrency {
export class WeChatPaySDK {}
export class ALiPaySDK {}
export class MeiTuanPaySDK {}
export class CreditCardPaySDK {}
}
export namespace VirtualCurrency {
export class QQCoinPaySDK {}
export class BitCoinPaySDK {}
export class ETHPaySDK {}
}
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
注意,这里的代码是在
.ts
文件中的,此时它是具有实际逻辑意义的,也不能和类型混作一谈。
而命名空间的使用类似于枚举:
const weChatPaySDK = new RealCurrency.WeChatPaySDK();
唯一需要注意的是,命名空间内部实际上就像是一个独立的代码文件,因此其中的变量需要导出以后,才能通过 RealCurrency.WeChatPaySDK
这样的形式访问。
如果你开始学习前端的时间较早,一定会觉得命名空间的编译产物很眼熟——它就像是上古时期里使用的伪模块化方案:
export var RealCurrency;
(function (RealCurrency) {
class WeChatPaySDK {
}
RealCurrency.WeChatPaySDK = WeChatPaySDK;
class ALiPaySDK {
}
RealCurrency.ALiPaySDK = ALiPaySDK;
class MeiTuanPaySDK {
}
RealCurrency.MeiTuanPaySDK = MeiTuanPaySDK;
class CreditCardPaySDK {
}
RealCurrency.CreditCardPaySDK = CreditCardPaySDK;
})(RealCurrency || (RealCurrency = {}));
2
3
4
5
6
7
8
9
10
11
12
13
14
15
实际上,命名空间的作用也正是实现简单的模块化功能,在 TypeScript 中引入它时(1.5 版本 (opens new window)),前端的模块化方案还处于混沌时期。
命名空间的内部还可以再嵌套命名空间,比如在虚拟货币中再新增区块链货币一类,此时嵌套的命名空间也需要被导出:
export namespace VirtualCurrency {
export class QQCoinPaySDK {}
export namespace BlockChainCurrency {
export class BitCoinPaySDK {}
export class ETHPaySDK {}
}
}
const ethPaySDK = new VirtualCurrency.BlockChainCurrency.ETHPaySDK();
2
3
4
5
6
7
8
9
10
11
类似于类型声明中的同名接口合并,命名空间也可以进行合并,但需要通过三斜线指令来声明导入。
// animal.ts
namespace Animal {
export namespace ProtectedAnimals {}
}
// dog.ts
/// <reference path="animal.ts" />
namespace Animal {
export namespace Dog {
export function bark() {}
}
}
// corgi.ts
/// <reference path="dog.ts" />
namespace Animal {
export namespace Dog {
export namespace Corgi {
export function corgiBark() {}
}
}
}
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
实际使用时需要导入全部的依赖文件:
/// <reference path="animal.ts" />
/// <reference path="dog.ts" />
/// <reference path="corgi.ts" />
Animal.Dog.Corgi.corgiBark();
2
3
4
5
除了在 .ts
文件中使用以外,命名空间也可以在声明文件中使用,即 declare namespace
:
declare namespace Animal {
export interface Dog {}
export interface Cat {}
}
declare let dog: Animal.Dog;
declare let cat: Animal.Cat;
2
3
4
5
6
7
8
但如果你在 @types/
系列的包下,想要通过 namespace 进行模块的声明,还需要注意将其导出,然后才会加载到对应的模块下。以 @types/react
为例:
export = React;
export as namespace React;
declare namespace React {
// 省略了不必要的类型标注
function useState<S>(initialState): [];
}
2
3
4
5
6
首先我们声明了一个命名空间 React,然后使用 export = React
将它导出了,这样我们就能够在从 react 中导入方法时,获得命名空间内部的类型声明,如 useState。
从这一个角度来看,declare namespace
其实就类似于普通的 declare
语法,只是内部的类型我们不再需要使用 declare
关键字(比如我们直接在 namespace 内部 function useState(): []
即可)。
而还有一行 export as namespace React
,它的作用是在启用了 --allowUmdGlobalAccess
配置的情况下,允许将这个模块作为全局变量使用(也就是不导入直接使用),这一特性同样也适用于通过 CDN 资源导入模块时的变量类型声明。
除了这两处 namespace 使用,React 中还利用 namespace 合并的特性,在全局的命名空间中注入了一些类型:
declare global {
namespace JSX {
interface Element extends React.ReactElement<any, any> { }
}
}
2
3
4
5
这也是为什么我们可以在全局使用 JSX.Element
作为类型标注。
除了类型声明中的导入——三斜线指令,以及类型声明中的模块——命名空间以外,TypeScript 还允许你将这些类型去导入到代码文件中。
# 仅类型导入
在 TypeScript 中,当我们导入一个类型时其实并不需要额外的操作,和导入一个实际值是完全一样的:
// foo.ts
export const Foo = () => {};
export type FooType = any;
// index.ts
import { Foo, FooType } from "./foo";
2
3
4
5
6
7
虽然类型导入和值导入存在于同一条导入语句中,在编译后的 JS 代码里还是只会有值导入存在,同时在编译的过程中,值与类型所在的内存空间也是分开的。
在这里我们只能通过名称来区分值和类型,但为每一个类型都加一个 Type 后缀也太奇怪了。实际上,我们可以更好地区分值导入和类型导入,只需要通过 import type
语法即可:
import { Foo } from "./foo";
import type { FooType } from "./foo";
2
这样会造成导入语句数量激增,如果你想同时保持较少的导入语句数量又想区分值和类型导入,也可以使用同一导入语句内的方式(需要 4.6 版本以后才支持):
import { Foo, type FooType } from "./foo";
这实际上是我个人编码习惯的一部分,即对导入语句块的规范整理。在大型项目中一个文件顶部有几十条导入语句是非常常见的,它们可能来自第三方库、UI库、项目内工具方法、样式文件、类型,项目内工具方法可能又分成 constants、hooks、utils、config 等等。如果将这些所有类型的导入都混乱地堆放在一起,对于后续的维护无疑是灾难。因此,我通常会将这些导入按照实际意义进行组织,顺序大致是这样:
- 一般最上面会是 React;
- 第三方 UI 组件,然后是项目内封装的其他组件;
- 第三方工具库,然后是项目内封装的工具方法,具体 hooks 和 utils 等分类的顺序可以按照自己偏好来;
- 类型导入,包括第三方库的类型导入、项目内的类型导入等;
- 样式文件,
CSS-IN-JS
方案的组件应该被放在第二条中其他组件部分。
示例如下:
import { useEffect } from 'react';
import { Button, Dialog } from 'ui';
import { ChildComp } from './child';
import { store } from '@/store'
import { useCookie } from '@/hooks/useCookie';
import { SOME_CONSTANTS } from '@/utils/constants';
import type { FC } from 'react';
import type { Foo } from '@/typings/foo';
import type { Shared } from '@/typings/shared';
import styles from './index.module.scss';
2
3
4
5
6
7
8
9
10
11
12
13
14
# 总结与预告
在这一节,我们主要了解了 TypeScript 在工程层面的基础能力,包括类型指令、类型声明、命名空间三个部分。
类型声明相关的能力几乎是所有规模的工程都会使用到的(你总会遇到没有提供类型定义的库吧),通过大量的额外类型声明我们可以实现更复杂、更准确的类型保护,以及为上古时期的 JavaScript npm 包提供类型定义,即 DefinitelyTyped。但类型指令却相反,它绝对不应该被滥用,无论是相当于后门的 ts-ignore
还是稍显安全的 ts-expect-error
。我们会在后面介绍如何通过 ESLint 规则来进行对应地约束。
而三斜线指令与命名空间这两个概念,虽然已经不再被大量使用,但了解它们诞生与存在的意义同样对理解整个 TypeScript 工程能力很有帮助。在下一节,我们还会与三斜线指令再次碰面。
无论你是在将 TypeScript 集成到什么框架或者工具里,其实你在做的只是一件事,那就是类型,类型,类型!。包括我们在下一节所要学习的 React 与 TypeScript 结合实战,其实本质上也是在学习如何让你的 React 组件也拥有可靠的类型支持。
# 扩展阅读
# 通过 JSDoc 在 JS 文件中获得类型提示
在上面我们提到了可以在 JS 文件中通过 JSDoc 来标注变量类型,而既然有了类型标注,那么自然也能享受到像 TS 文件中一样的类型提示了。但这里我们需要使用更强大一些的 JSDoc 能力:在 @type {}
中使用导入语句!
以拥有海量配置项的 Webpack 为例:
/** @type {import("webpack").Configuration} */
const config = {};
module.exports = config;
2
3
4
此时你会发现已经拥有了如臂使指的类型提示:
类似的,也可以直接进行导出:
module.exports = /** @type { import('webpack').Configuration } */ ({});
当然,Webpack 本身也支持通过 ts 文件进行配置,在使用 TS 进行配置时,一种方式是简单地使用它提供的类型作为一个对象的标注。而目前更常见的一种方式其实是框架内部提供 defineConfig
这样的方法,让你能直接获得类型提示,如 Vite 中的做法:
import { defineConfig } from 'vite'
import react from '@vitejs/plugin-react'
export default defineConfig({
plugins: [react()]
})
2
3
4
5
6