TypeScript基础知识点
- 一、TypeScript概述
- 1.产生背景
- 2.主要功能
- 3.主要特性
- 4.开发工具
- 5.TypeScript 开源
- 6.TypeScript 5.0正式发布!(2023年3月17日)
- 6.1 TypeScript 5.0的安装
- 6.2 TypeScript 5.0 的主要更新
- 7. 5分钟用TypeScript创建一个简单的Web应用
- 8.TypeScript基础类型
- 9.TypeScript变量声明
- 10. 接口
- 11. 类
- 12. 函数
- 13. 泛型
- 14. 枚举
- 15. 类型推论
- 16. 类型兼容性
- 17. 高级类型
- 18. Symbols
- 19. 迭代器和生成器
- 20. 模块
- 21. 命名空间
- 22. 命名空间和模块
- 23. 模块解析
- 24. 声明合并
- 25. JSX
- 26. 装饰器
- 27. Mixins
- 28. 三斜线指令
- 29. JavaScript文件类型检查
- 总结
一、TypeScript概述
TypeScript是微软开发的一个开源的编程语言,通过在JavaScript的基础上添加静态类型定义构建而成。
TypeScript通过TypeScript编译器或Babel转译为JavaScript代码,可运行在任何浏览器,任何操作系统。
TypeScript添加了很多尚未正式发布的ECMAScript新特性(如装饰器)。
2012年10月,微软发布了首个公开版本的TypeScript,2013年6月19日,在经历了一个预览版之后微软正式发布了正式版TypeScript。当前最新正式版本为TypeScript 5.2, 2023年8月发布。
1.产生背景
TypeScript 打破了 JavaScript 的局限性
JavaScript 是世界上最常用的编程语言之一,已成为 Web 官方语言。 开发人员使用它来编写可在任何平台和任何浏览器中运行的跨平台应用程序。
尽管 JavaScript 用于创建跨平台应用,但它并不是为涉及数千甚至数百万行代码的大型应用设计的。 JavaScript 缺少一些更成熟的语言所具备的功能,这些功能可为当今复杂的应用程序提供支持。 用集成开发编辑器 (IDE) 管理 JavaScript 和维护这些大型代码库颇具挑战。
TypeScript 打破了 JavaScript 的局限性,且不会因此影响 JavaScript 的关键价值主张:能够在任何地方使用任何平台、浏览器或主机运行代码。
TypeScript 起源于使用JavaScript开发的大型项目 。由于JavaScript语言本身的局限性,难以胜任大型项目的开发和维护。因此微软开发了TypeScript ,使得其能够胜任大型项目的开发。
2.主要功能
TypeScript的作者是安德斯·海尔斯伯格,C#的首席架构师。它是开源和跨平台的编程语言。它是JavaScript的一个超集,而且本质上向这个语言添加了可选的静态类型和基于类的面向对象编程。
TypeScript扩展了JavaScript的语法,所以任何现有的JavaScript程序可以运行在TypeScript环境中。TypeScript是为大型应用的开发而设计,并且可以编译为JavaScript。
TypeScript 支持为已存在的 JavaScript 库添加类型信息的头文件,扩展了它对于流行库的支持,如 jQuery,MongoDB,Node.js 和 D3.js 等。这些第三方库的类型定义本身也是开源的,所有开发者都能参与贡献。
2.1 TypeScript 与 JavaScript
2.1.1 TypeScript 与 JavaScript的关系
JavaScript 是世界上最常用的编程语言之一,已成为 Web 官方语言。 开发人员使用它来编写可在任何平台和任何浏览器中运行的跨平台应用程序。
尽管 JavaScript 用于创建跨平台应用,但它并不是为涉及数千甚至数百万行代码的大型应用设计的。 JavaScript 缺少一些更成熟的语言所具备的功能,这些功能可为当今复杂的应用程序提供支持。 用集成开发编辑器 (IDE) 管理 JavaScript 和维护这些大型代码库颇具挑战。
TypeScript 打破了 JavaScript 的局限性,且不会因此影响 JavaScript 的关键价值主张:能够在任何地方使用任何平台、浏览器或主机运行代码。
TypeScript 是由 Microsoft 开发的一种开放源代码语言。 它是 JavaScript 的一个超集,这意味着你可以使用已开发的 JavaScript 技能,以及以前不可用的某些功能。
2.1.2 TypeScript 与 JavaScript的不同
对比 | TypeScript | JavaScript |
---|---|---|
类型系统 | Typescript 是一种强类型化面向对象的编译语言。它是由微软开发的。 | JavaScript是一种轻量级的解释型语言。它是由Netscape推出的。 |
实施端 | Typescript的内部实现不允许在服务器端使用它。它只能在客户端使用。 | JavaScript 可以在客户端和服务器端使用。 |
数据绑定 | 为了在代码级别绑定数据,Typescript 使用类型和接口等概念来描述正在使用的数据。 | 在JavaScript中没有引入这样的概念。 |
汇编 | 用TypeScript编写的代码首先需要编译,然后转换为JavaScript。此转换过程称为转译。 | 在JavaScript的情况下不需要编译。 |
模块化编程 | TypeScript支持模块,因此它允许模块化编程。 | JavaScript不支持模块,因此它不允许模块化编程。 |
函数中的可选参数 | 在用 Typescript 编写的函数代码中允许任意数量的可选参数。 | JavaScript 不支持可选参数函数。 |
应用方向 | JavaScript 的超集用于解决大型项目的代码复杂性。 | 一种脚本语言,用于创建动态网页。 |
发现错误时间 | 可以在编译期间发现并纠正错误。 | 作为一种解释型语言,只能在运行时发现错误。 |
语法 | 静态类型注解,类和接口定义,枚举类型,装饰器等 | 基本的面向对象语法,原型链继承,匿名函数等 |
类型系统 | 强大的静态类型系统,类型推断,类型注解,联合类型,交叉类型,泛型支持,严格的类型检查 | 动态类型系统,基于值的类型推断,灵活但容易出错 |
工具支持 | 强大的编辑器支持(如 Visual Studio Code),代码补全,类型检查,重构功能,错误提示,智能感知,自动导入等 | 基本的编辑器支持,某些编辑器提供基本的代码补全和错误提示 |
类型声明文件支持 | 丰富的类型声明文件生态系统,用于描述 JavaScript 库的类型,提供了良好的第三方库和框架的类型定义 | JavaScript 库的类型声明文件相对较少,需要手动编写或通过社区维护 |
类型安全 | 静态类型检查使得编码过程中能够尽早发现类型错误和潜在问题,提高代码质量和可维护性 | 动态类型系统导致类型错误只能在运行时才能发现,可通过测试覆盖率、代码质量工具等方式减少错误出现的概率 |
性能 | 编译过程会引入额外的开销,但生成的 JavaScript 代码在运行时性能与直接编写的 JavaScript 代码相当 | 更简单的解释执行,没有额外的编译开销,运行效率相对较高 |
迁移成本 | 在现有 JavaScript 项目中引入 TypeScript 需要进行一定的迁移工作,并对代码进行类型注解。但可以逐步完成迁移过程,避免一次性的大规模重构 | 无需迁移成本,现有的 JavaScript 代码可以直接运行 |
社区和生态系统 | 日益壮大的社区,活跃的开发者社区,丰富的第三方库和工具支持,官方和社区维护的文档和教程 | 世界最大的开源生态系统之一,庞大的开发者社区和资源 |
适用场景 | 大型项目和团队合作,需要更强类型安全性和工具支持的项目;前端框架和库开发,需要构建复杂的应用逻辑和可重用的组件;需要长期维护的项目;与其他静态类型语言集成的项目 | 快速原型开发,小型项目,脚本编写,前端开发中一次性使用的简单脚本等 |
2.1.3 TypeScript 与 JavaScript 的兼容性
TypeScript 是 ECMAScript 2015(ECMAScript 6 或 ES6)的严格超集。 此关系意味着所有 JavaScript 代码也是 TypeScript 代码,而 TypeScript 程序可以无缝地使用 JavaScript。
浏览器仅理解 JavaScript。 若要使应用程序正常工作,则在 TypeScript 中编写应用程序时,需要编译代码并将其转换为 JavaScript。 使用 TypeScript 编译器或兼容 TypeScript 的转译器,可以将 TypeScript 代码转换为 JavaScript 代码。 生成的 JavaScript 是干净简单的代码,可在 JavaScript 运行的任何地方运行:在浏览器中、Node.js 上或应用中。
重要
使用 TypeScript 时,请记住在几乎所有情况下,TypeScript 都将被编译(或转译)成 JavaScript,而 JavaScript 实际上由运行时执行。 你可以在使用 JavaScript 的任何项目上使用 TypeScript。
3.主要特性
TypeScript 是一种给 JavaScript 添加特性的语言扩展。
- 类型批注和编译时类型检查
- 类
- 接口
- 模块
- 装饰器
语法上,TypeScript 很类似于 JScript .NET,另外一个添加了对静态类型,经典的面向对象语言特性如类,继承,接口和命名空间等的支持的 Microsoft 对 ECMAScript 语言标准的实现。
3.1 类型批注
TypeScript 通过类型批注提供静态类型以在编译时启动类型检查。这是可选的,而且可以被忽略而使用 JavaScript 常规的动态类型。
3.1.1 TypeScript代码案例:Add函数
function add(left:number,right:number);number{
return left + right;
}
对于基本类型的批注是 number, bool 和 string。而弱或动态类型的结构则是 any 类型。
类型批注可以被导出到一个单独的声明文件以让使用类型的已被编译为 JavaScript 的 TypeScript 脚本的类型信息可用。批注可以为一个现有的 JavaScript 库声明,就像已经为 Node.js 和 jQuery 所做的那样。
当类型没有给出时,TypeScript 编译器利用类型推断以推断类型。如果由于缺乏声明,没有类型可以被推断出,那么它就会默认为是动态的 any 类型。
3.2 声明文件
当一个 TypeScript 脚本被编译时,有一个产生作为编译后的 JavaScript 的组件的一个接口而起作用的声明文件 (具有扩展名 .d.ts) 的选项。在这个过程中编译器基本上带走所有的函数和方法体而仅保留所导出类型的批注。当第三方开发者从 TypeScript 中使用它时,由此产生的声明文件就可以被用于描述一个 JavaScript 库或模块导出的虚拟的 TypeScript 类型。
声明文件的概念类似于 C/C++ 中头文件的概念。
3.2.1 TypeScript代码案例:模块
module arithmetics {
add(left:number,right:number):number;
subtract(left:number,right:number):number;
multiply(left:number,right:number):number;
divide(left:number,right:number):number;
}
类型声明文件可以为已存在的 JavaScript 库手写,就像为 jQuery 和 Node.js 所做的那样。
对 ECMAScript 6 的支持
TypeScript 增加了对为即将到来的 ECMAScript 6 标准所建议的特性的支持。
如下为其构想:
类 (以及继承) 模块Arrow functions
尽管标准还未准备就绪,Microsoft 说它的目标是使 TypeScript 的特性与建议的标准看齐。
类
TypeScript 支持集成了可选的类型批注支持的 ECMAScript6 的类。
3.2.2 TypeScript 代码案例:类Class
class person {
private name:string;
private age:number;
construvtor(name:string,age:number) {
this.name = name;
this.age = age;
}
tostring():string{
return this.name + "(" + this.age +")";
}
}
3.3 泛型
这种语言的规范说明一个未来的版本将会支持基于类型擦除的泛型编程。
4.开发工具
TypeScript 编译器,名称叫 tsc, 是用可以被编译为可以被执行在任何 JavaScript 引擎中,在任何宿主 - 如浏览器 - 中的常规 JavaScript 的 TypeScript 写的。编译器包被绑定于一个可以执行编译器的脚本宿主。使用 Node.js 作为宿主的 Node.js 包同样可以获得。
也有用 JavaScript 写的客户端编译器的一个 alpha 版本,它在页面载入时,实时执行 JavaScript 代码。
这种编译器的当前版本默认支持 ECMAScript 3。一个选项是允许以 ECMAScript 5 为目标以利用该版本独有的语言特性。类,尽管是 ECMAScript 6 标准的一部分,在这两个模式下都可用。
4.1 IDE 和编辑器支持
微软官方推荐的编辑器有:
4.1.1 Visual Studio 2019
4.1.2 Visual Studio Code
4.1.3 Visual Studio 2017
5.TypeScript 开源
TypeScript 是开源的,其源代码可以在 Apache 2 License 下从 CodePlex 获得。这个项目由 Microsoft 维持,但是任何人可以通过经 CodePlex 项目页发送反馈,建议和 bugfixes 而做出贡献。
已有一些批评提到这一想法,即使 TypeScript 鼓励强类型,当前也只有 Microsoft Visual Studio 允许为该语言容易的开发。最初的观点是在其它的编辑器上带来强类型,IntelliSense, 代码完成和代码重构可能不是一个简单的任务。此外,允许为 TypeScript 开发的 Visual Studio 扩展不是开源的。
最好的 TypeScript 开发体验是在 Microsoft Windows 上, 然而随着时间的流逝以及这种语言开放的本质,加之编译器自我托管,而且用 TypeScript 自身写的,这很有可能会改变。可以通过编译器的源代码访问到 AST (抽象句法树),也可以获得详细的语言规范文档,社区已开始构建一个跨平台的编辑器,利用和 Visual Studio 所用相同的语言服务以提供一个增强的编辑体验。编辑器仍然在概念检验的阶段,但已经运行于 Linux, OSX 和 Windows,提供针对之前对提供此类服务的困难度的估计的 IntelliSense, 代码完成和句法高亮。
6.TypeScript 5.0正式发布!(2023年3月17日)
2023 年 3 月 17 日,TypeScript 5.0 正式发布!此版本带来了许多新功能,旨在使 TypeScript 更小、更简单、更快。TypeScript 5.0 实现了新的装饰器标准、更好地支持 Node 和打构建工具中的 ESM 项目的功能、库作者控制泛型推导的新方法、扩展了 JSDoc 功能、简化了配置,并进行了许多其他改进。
6.1 TypeScript 5.0的安装
通过以下 npm 命令开始使用 TypeScript 5.0:
npm install -D typescript
6.2 TypeScript 5.0 的主要更新
6.2.1 全新装饰器
装饰器是即将推出的 ECMAScript 特性,它允许我们以可重用的方式自定义类及其成员。
考虑以下代码:
class Person {
name: string;
constructor(name: string) {
this.name = name;
}
greet() {
console.log(`Hello, my name is ${this.name}.`);
}
}
const p = new Person("Ray");
p.greet();
这里的 greet 方法很简单,在实际中它内部可能会跟复杂,比如需要执行异步逻辑,或者进行递归,亦或是有副作用等。那就可能需要使用 console.log 来调试 greet:
class Person {
name: string;
constructor(name: string) {
this.name = name;
}
greet() {
console.log("LOG: Entering method.");
console.log(`Hello, my name is ${this.name}.`);
console.log("LOG: Exiting method.")
}
}
如果有一种方法可以为每种方法做到这一点,可能会很好。
这就是装饰器的用武之地。我们可以编写一个名为 loggedMethod 的函数,如下所示:
function loggedMethod(originalMethod: any, _context: any) {
function replacementMethod(this: any, ...args: any[]) {
console.log("LOG: Entering method.")
const result = originalMethod.call(this, ...args);
console.log("LOG: Exiting method.")
return result;
}
return replacementMethod;
}
这里用了很多 any,可以暂时忽略,这样可以让例子尽可能得简单。
这里,loggedMethod 需要传入一个参数(originalMethod) 并返回一个函数。执行过程如下:
打印:LOG: Entering method.
将 this 及其所有参数传递给原始方法
打印:LOG: Exiting method.
返回原始方法的执行结果
现在我们就可以使用 loggedMethod 来修饰 greet 方法:
class Person {
name: string;
constructor(name: string) {
this.name = name;
}
@loggedMethod
greet() {
console.log(`Hello, my name is ${this.name}.`);
}
}
const p = new Person("Ray");
p.greet();
输出如下:
LOG: Entering method.
Hello, my name is Ray.
LOG: Exiting method.
这里我们在 greet 上面使用了 loggedMethod 作为装饰器——注意这里的写法:@loggedMethod。这样,它会被原始方法和 context 对象调用。因为 loggedMethod 返回了一个新函数,该函数替换了 greet 的原始定义。
loggedMethod 的第二个参数被称为“ context 对象”,它包含一些关于如何声明装饰方法的有用信息——比如它是 #private 成员还是静态成员,或者方法的名称是什么。下面来重写 loggedMethod 以利用它并打印出被修饰的方法的名称。
function loggedMethod(originalMethod: any, context: ClassMethodDecoratorContext) {
const methodName = String(context.name);
function replacementMethod(this: any, ...args: any[]) {
console.log(`LOG: Entering method '${methodName}'.`)
const result = originalMethod.call(this, ...args);
console.log(`LOG: Exiting method '${methodName}'.`)
return result;
}
return replacementMethod;
}
TypeScript 提供了一个名为 ClassMethodDecoratorContext 的类型,它对方法装饰器采用的 context 对象进行建模。除了元数据之外,方法的 context 对象还有一个有用的函数:addInitializer。这是一种挂接到构造函数开头的方法(如果使用静态方法,则挂接到类本身的初始化)。
举个例子,在JavaScript中,经常会写如下的模式:
class Person {
name: string;
constructor(name: string) {
this.name = name;
this.greet = this.greet.bind(this);
}
greet() {
console.log(`Hello, my name is ${this.name}.`);
}
}
或者,greet可以声明为初始化为箭头函数的属性。
class Person {
name: string;
constructor(name: string) {
this.name = name;
}
greet = () => {
console.log(`Hello, my name is ${this.name}.`);
};
}
编写这段代码是为了确保在greet作为独立函数调用或作为回调函数传递时不会重新绑定。
const greet = new Person("Ray").greet;
greet();
可以编写一个装饰器,使用addInitializer在构造函数中为我们调用 bind。
function bound(originalMethod: any, context: ClassMethodDecoratorContext) {
const methodName = context.name;
if (context.private) {
throw new Error(`'bound' cannot decorate private properties like ${methodName as string}.`);
}
context.addInitializer(function () {
this[methodName] = this[methodName].bind(this);
});
}
bound不会返回任何内容,所以当它装饰一个方法时,它会保留原来的方法。相反,它会在其他字段初始化之前添加逻辑。
class Person {
name: string;
constructor(name: string) {
this.name = name;
}
@bound
@loggedMethod
greet() {
console.log(`Hello, my name is ${this.name}.`);
}
}
const p = new Person("Ray");
const greet = p.greet;
greet();
注意,我们使用了两个装饰器:@bound和@loggedMethod。这些装饰是以“相反的顺序”运行的。也就是说,@loggedMethod修饰了原始方法greet, @bound修饰了@loggedMethod的结果。在这个例子中,这没有关系——但如果装饰器有副作用或期望某种顺序,则可能有关系。
可以将这些装饰器放在同一行:
@bound @loggedMethod greet() {
console.log(`Hello, my name is ${this.name}.`);
}
我们甚至可以创建返回装饰器函数的函数。这使得我们可以对最终的装饰器进行一些自定义。如果我们愿意,我们可以让loggedMethod返回一个装饰器,并自定义它记录消息的方式。
function loggedMethod(headMessage = "LOG:") {
return function actualDecorator(originalMethod: any, context: ClassMethodDecoratorContext) {
const methodName = String(context.name);
function replacementMethod(this: any, ...args: any[]) {
console.log(`${headMessage} Entering method '${methodName}'.`)
const result = originalMethod.call(this, ...args);
console.log(`${headMessage} Exiting method '${methodName}'.`)
return result;
}
return replacementMethod;
}
}
如果这样做,必须在使用loggedMethod作为装饰器之前调用它。然后,可以传入任何字符串作为记录到控制台的消息的前缀。
class Person {
name: string;
constructor(name: string) {
this.name = name;
}
@loggedMethod("")
greet() {
console.log(`Hello, my name is ${this.name}.`);
}
}
const p = new Person("Ray");
p.greet();
输出结果如下:
Entering method 'greet'.
Hello, my name is Ray.
Exiting method 'greet'.
装饰器可不仅仅用于方法,还可以用于属性/字段、getter、setter和自动访问器。甚至类本身也可以装饰成子类化和注册。
上面的loggedMethod和bound装饰器示例写的很简单,并省略了大量关于类型的细节。实际上,编写装饰器可能相当复杂。例如,上面的loggedMethod类型良好的版本可能看起来像这样:
function loggedMethod<This, Args extends any[], Return>(
target: (this: This, ...args: Args) => Return,
context: ClassMethodDecoratorContext<This, (this: This, ...args: Args) => Return>
) {
const methodName = String(context.name);
function replacementMethod(this: This, ...args: Args): Return {
console.log(`LOG: Entering method '${methodName}'.`)
const result = target.call(this, ...args);
console.log(`LOG: Exiting method '${methodName}'.`)
return result;
}
return replacementMethod;
}
我们必须使用this、Args和return类型参数分别建模this、参数和原始方法的返回类型。
具体定义装饰器函数的复杂程度取决于想要保证什么。需要记住,装饰器的使用次数将超过它们的编写次数,所以类型良好的版本通常是更好的——但显然与可读性有一个权衡,所以请尽量保持简单。
6.2.2 const 类型参数
当推断一个对象的类型时,TypeScript通常会选择一个通用类型。例如,在本例中,names 的推断类型是string[]:
type HasNames = { readonly names: string[] };
function getNamesExactly<T extends HasNames>(arg: T): T["names"] {
return arg.names;
}
// names 的推断类型为 string[]
const names = getNamesExactly({ names: ["Alice", "Bob", "Eve"]});
通常这样做的目的是实现突变。然而,根据getnames确切的作用以及它的使用方式,通常情况下需要更具体的类型。到目前为止,通常不得不在某些地方添加const,以实现所需的推断:
// 我们想要的类型: readonly ["Alice", "Bob", "Eve"]
// 我们得到的类型: string[]
const names1 = getNamesExactly({ names: ["Alice", "Bob", "Eve"]});
// 得到想要的类型:readonly ["Alice", "Bob", "Eve"]
const names2 = getNamesExactly({ names: ["Alice", "Bob", "Eve"]} as const);
这写起来会很麻烦,也很容易忘记。在 TypeScript 5.0 中,可以在类型参数声明中添加const修饰符,从而使类const推断成为默认值:
type HasNames = { names: readonly string[] };
function getNamesExactly<const T extends HasNames>(arg: T): T["names"] {
// ^^^^^
return arg.names;
}
// 推断类型:readonly ["Alice", "Bob", "Eve"]
// 注意,这里不需要再写 as const
const names = getNamesExactly({ names: ["Alice", "Bob", "Eve"] });
注意,const修饰符并不排斥可变值,也不需要不可变约束。使用可变类型约束可能会得到意外的结果。例如:
declare function fnBad<const T extends string[]>(args: T): void;
// T仍然是string[],因为readonly ["a", "b", "c"]不能赋值给string[]
fnBad(["a", "b" ,"c"]);
这里,T的推断候选值是readonly [“a”, “b”, “c”],而readonly数组不能用于需要可变数组的地方。在这种情况下,推理回退到约束,数组被视为string[],调用仍然成功进行。
更好的定义应该使用readonly string[]:
declare function fnGood<const T extends readonly string[]>(args: T): void;
// T 是 readonly ["a", "b", "c"]
fnGood(["a", "b" ,"c"]);
同样,要记住,const修饰符只影响在调用中编写的对象、数组和基本类型表达式的推断,所以不会(或不能)用const修饰的参数将看不到任何行为的变化:
declare function fnGood<const T extends readonly string[]>(args: T): void;
const arr = ["a", "b", "c"];
// T 仍然是 string[],const 修饰符没有作用
fnGood(arr);
// 这是因为const修饰符主要用于确保类型参数T是一个只读元组类型,即T的元素数量和顺序都是固定的,且元素不可以被重新赋值。
// 但当我们传入一个已经定义好的常量数组时,TypeScript编译器无法从该数组本身推断出它是否是一个只读元组。
// 实际上,const修饰符在这里主要影响的是那些在函数体内部创建的字面量数组,它确保这些字面量数组保持只读且其类型不会被扩展为更泛化的类型。
// 例如,如果我们直接在函数调用中创建一个数组字面量,const修饰符将发挥作用:
declare function fnWithLiteral<const T extends readonly string[]>(args: T): void;
// 下面的调用中,T将被推断为具体的只读元组类型['a', 'b', 'c']
fnWithLiteral(['a', 'b', 'c']);
// 在这个例子中,由于我们直接在函数调用中使用了数组字面量,TypeScript能够推断出这是一个具体的元组类型,而不是一个更泛化的string[]类型。
// 因此,如果尝试改变数组元素的顺序或添加新的元素,TypeScript将报错,因为const修饰符确保了元组的只读性。
// 总结来说,const修饰符在泛型函数参数上的作用主要是为了确保传入的数组字面量保持只读元组的特性,而不是影响已经定义好的常量数组的类型推断。
// 在处理已经定义好的数组时,我们仍然需要依赖其他方式来确保数组的不可变性,例如使用ReadonlyArray<T>或Readonly<T[]>等类型。
在上述例子中,我们详细讨论了const
修饰符在TypeScript泛型函数参数中的行为。它主要用于确保字面量数组在函数内部保持只读元组的特性,而不是用于影响已经定义好的常量数组的类型推断。了解这一点有助于我们更好地使用const
修饰符,并在TypeScript中编写出更加健壮和类型安全的代码。
6.2.3 extends 支持多配置文件
当管理多个项目时,通常每个项目的 tsconfig.json 文件都会继承于基础配置。这就是为什么TypeScript支持extends字段,用于从compilerOptions中复制字段。
// packages/front-end/src/tsconfig.json
{
"extends": "../../../tsconfig.base.json",
"compilerOptions": {
"outDir": "../lib",
// ...
}
}
但是,在某些情况下,可能希望从多个配置文件进行扩展。例如,想象一下使用一个TypeScript 基本配置文件到 npm。如果想让所有的项目也使用npm中@tsconfig/strictest包中的选项,那么有一个简单的解决方案:将tsconfig.base.json扩展到@tsconfig/strictest:
// tsconfig.base.json
{
"extends": "@tsconfig/strictest/tsconfig.json",
"compilerOptions": {
// ...
}
}
这在一定程度上是有效的。如果有任何项目不想使用 @tsconfig/strictest,就必须手动禁用这些选项,或者创建一个不从 @tsconfig/strictest 扩展的单独版本的 tsconfig.base.json。
为了提供更多的灵活性,Typescript 5.0 允许extends字段接收多个项。例如,在这个配置文件中:
{
"extends": ["a", "b", "c"],
"compilerOptions": {
// ...
}
}
这样写有点像直接扩展 c,其中 c 扩展 b,b 扩展 a。如果任何字段“冲突”,则后一个项生效。
所以在下面的例子中,strictNullChecks 和 noImplicitAny 都会在最终的 tsconfig.json 中启用。
// tsconfig1.json
{
"compilerOptions": {
"strictNullChecks": true
}
}
// tsconfig2.json
{
"compilerOptions": {
"noImplicitAny": true
}
}
// tsconfig.json
{
"extends": ["./tsconfig1.json", "./tsconfig2.json"],
"files": ["./index.ts"]
}
可以用下面的方式重写最上面的例子:
// packages/front-end/src/tsconfig.json
{
"extends": ["@tsconfig/strictest/tsconfig.json", "../../../tsconfig.base.json"],
"compilerOptions": {
"outDir": "../lib",
// ...
}
}
6.2.4 所有枚举都是联合枚举
当 TypeScript 最初引入枚举时,它只不过是一组具有相同类型的数值常量:
enum E {
Foo = 10,
Bar = 20,
}
E.Foo 和 E.Bar 唯一的特别之处在于它们可以分配给任何期望类型 E 的东西。除此之外,它们只是数字。
function takeValue(e: E) {}
takeValue(E.Foo); // ✅
takeValue(123); // ❌
直到 TypeScript 2.0 引入了枚举字面量类型,它赋予每个枚举成员自己的类型,并将枚举本身转换为每个成员类型的联合。它还允许我们只引用枚举类型的一个子集,并缩小这些类型。
// Color就像是一个联合:Red | Orange | Yellow | Green | Blue | Violet
enum Color {
Red, Orange, Yellow, Green, Blue, /* Indigo */, Violet
}
// 每个枚举成员都有自己的类型,可以引用
type PrimaryColor = Color.Red | Color.Green | Color.Blue;
function isPrimaryColor(c: Color): c is PrimaryColor {
// 缩小字面量类型可以捕获bug
// TypeScript在这里会报错,因为
// 最终会比较 Color.Red 和 Color.Green。
// 本想使用||,但不小心写了&&
return c === Color.Red && c === Color.Green && c === Color.Blue;
}
给每个枚举成员指定自己的类型有一个问题,即这些类型在某种程度上与成员的实际值相关联。在某些情况下,这个值是不可能计算出来的——例如,枚举成员可以通过函数调用进行初始化。
enum E {
Blah = Math.random()
}
每当TypeScript遇到这些问题时,它都会悄无声息地退出并使用旧的枚举策略。这意味着要放弃并集和字面量类型的所有优点。
TypeScript 5.0 通过为每个计算成员创建唯一的类型,设法将所有枚举转换为联合枚举。这意味着现在可以缩小所有枚举的范围,并将其成员作为类型引用。
6.2.5 --moduleResolutionbundler
TypeScript 4.7 为 --module 和 --moduleResolution 设置引入了 node16 和 nodenext 选项。这些选项的目的是更好地模拟 Node.js 中 ECMAScript 模块的精确查找规则;然而,这种模式有许多其他工具没有真正执行的限制。
例如,在 Node.js 的 ECMAScript 模块中,任何相对导入都需要包含文件扩展名。
// entry.mjs
import * as utils from "./utils"; // ❌ - 需要包括文件扩展名。
import * as utils from "./utils.mjs"; // ✅
在Node.js和浏览器中这样做是有原因的——它使文件查找更快,并且更适合原始文件服务器。但对于许多使用打包工具的开发人员来说,node16/nodenext 的设置很麻烦,因为打包工具没有这些限制中的大部分。在某些方面,node解析模式更适合使用打包工具的人。
但在某些方面,原有的 node 解析模式已经过时了。大多数现代打包工具在 Node.js 中使用 ECMAScript 模块和 CommonJS 查找规则的融合。
为了模拟打包工具是如何工作的,TypeScript 5.0 引入了一个新策略:–moduleResolution bundler
{
"compilerOptions": {
"target": "esnext",
"moduleResolution": "bundler"
}
}
如果正在使用现代打包工具,如 Vite、esbuild、swc、Webpack、Parcel 或其他实现混合查找策略的打包工具,那么新的 bundler 选项应该非常适合你。
另一方面,如果正在编写一个打算在 npm 上发布的库,使用bundler选项可以隐藏不使用bundler的用户可能出现的兼容性问题。因此,在这些情况下,使用node16或nodenext解析选项可能是更好的方法。
6.2.6 自定义解析标志
JavaScript 工具现在可以模拟“混合”解析规则,就像上面描述的打包工具模式一样。由于工具的支持可能略有不同,TypeScript 5.0 提供了启用或禁用一些功能的方法。
6.2.6.1 allowImportingTsExtensions
–allowImportingTsExtensions 允许 TypeScript 文件使用特定于 TypeScript 的扩展名(如 .ts、.mts 或 .tsx)相互导入。
仅当启用 --noEmit 或 --emitDeclarationOnly 时才允许使用此标志,因为这些导入路径在运行时无法在 JavaScript 输出文件中解析。这里的期望是解析器(例如打包工具、运行时或其他工具)将使 .ts 文件之间的这些导入正常工作。
6.2.6.2 resolvePackageJsonExports
–resolvePackageJsonExports 强制 TypeScript 在从 node_modules 中的包中读取时查询 package.json 文件的 exports 字段。
6.2.6.3 resolvePackageJsonImports
–resolvePackageJsonImports 强制 TypeScript 在从其祖先目录包含 package.json 的文件执行以 # 开头的查找时查询 package.json 文件的 imports 字段。
在 --moduleResolution 的 node16、nodenext 和 bundler 选项下,此选项默认为 true。
6.2.6.4 allowArbitraryExtensions
在 TypeScript 5.0 中,当导入路径以不是已知 JavaScript 或 TypeScript 文件扩展名的扩展名结尾时,编译器将以 {file basename}.d.{extension} 的形式查找该路径的声明文件。例如,如果在打包项目中使用 CSS loader,可能希望为这些样式表编写(或生成)声明文件:
/* app.css */
.cookie-banner {
display: none;
}
// app.d.css.ts
declare const css: {
cookieBanner: string;
};
export default css;
// App.tsx
import styles from "./app.css";
styles.cookieBanner; // string
默认情况下,这个导入将引发一个错误,让你知道TypeScript不理解这个文件类型,你的运行时可能不支持导入它。但是,如果已经配置了运行时或打包工具来处理它,则可以使用新–allowArbitraryExtensions编译器选项来抑制错误。
注意,可以通过添加一个名为 app.css.d.ts 而不是 app.d.css.ts 的声明文件通常可以实现类似的效果。然而,这只是通过 Node 对 CommonJS 的 require 解析规则实现的。严格来说,前者被解释为一个名为 app.css.js 的 JavaScript 文件的声明文件。因为相关文件导入需要在 Node 的 ESM 支持中包含扩展名,所以在我们的例子中,TypeScript 会在 --moduleResolution node16 或 nodenext 下的 ESM 文件中出错。
customConditions
–customConditions 获取当 TypeScript 从 package.json 的 [exports] 或 (https://nodejs.org/api/packages.html#exports)) 或 imports 字段解析时应该成功的附加的条件列表。这些条件将添加到解析器默认使用的现有条件中。
例如,当此字段在 tsconfig.json 中设置为:
{
"compilerOptions": {
"target": "es2022",
"moduleResolution": "bundler",
"customConditions": ["my-condition"]
}
}
任何时候在 package.json 中引用 exports 或 imports 字段时,TypeScript 都会考虑名为 my-condition 的条件。
因此,当从具有以下 package.json 的包中导入时:
{
// ...
"exports": {
".": {
"my-condition": "./foo.mjs",
"node": "./bar.mjs",
"import": "./baz.mjs",
"require": "./biz.mjs"
}
}
}
TypeScript 将尝试查找与foo.mjs对应的文件。这个字段只有在 node16、nodenext 和–modulerresolution为 bundler 时才有效。
6.2.7 --verbatimModuleSyntax
默认情况下,TypeScript 会执行一些称为导入省略的操作。如果这样写:
import { Car } from "./car";
export function drive(car: Car) {
// ...
}
TypeScript 检测到只对类型使用导入并完全删除导入。输出 JavaScript 可能是这样的:
export function drive(car) {
// ...
}
大多数时候这很好,因为如果 Car 不是从 ./car 导出的值,将得到一个运行时错误。但对于某些边界情况,它确实增加了一层复杂性。例如,没有像 import “./car” 这样的语句,即完全放弃了 import,这实际上对有无副作用的模块产生影响。
TypeScript 的 JavaScript emit 策略也有另外几层复杂性——省略导入并不总是由如何使用 import 驱动的,它通常还会参考值的声明方式。所以并不总是很清楚是否像下面这样的代码:
export { Car } from "./car";
如果 Car 是用类之类的东西声明的,那么它可以保存在生成的 JavaScript 文件中。但是,如果 Car 仅声明为类型别名或接口,则 JavaScript 文件不应导出 Car。
虽然 TypeScript 可能能够根据来自跨文件的信息做出这些发出决策,但并非每个编译器都可以。
imports 和 exports 的类型修饰符在这些情况下会有帮助。我们可以明确指定import或export仅用于类型分析,并且可以在JavaScript文件中使用类型修饰符完全删除。
// 这条语句可以在JS输出中完全删除
import type * as car from "./car";
// 在JS输出中可以删除命名的import/export Car
import { type Car } from "./car";
export { type Car } from "./car";
类型修饰符本身并不是很有用——默认情况下,模块省略仍然会删除导入,并且没有强制区分类型和普通导入和导出。因此 TypeScript 有标志 --importsNotUsedAsValues 以确保使用 type 修饰符,–preserveValueImports 以防止某些模块省略行为,以及 --isolatedModules 以确保 TypeScript 代码适用于不同的编译器。不幸的是,很难理解这 3 个标志的细节,并且仍然存在一些具有意外行为的边界情况。
TypeScript 5.0 引入了一个名为 --verbatimModuleSyntax 的新选项来简化这种情况。规则要简单得多,任何没有 type 修饰符的导入或导出都会被保留。任何使用 type 修饰符的内容都会被完全删除。
// 完全被删除
import type { A } from "a";
// 重写为 'import { b } from "bcd";'
import { b, type c, type d } from "bcd";
// 重写为 'import {} from "xyz";'
import { type xyz } from "xyz";
有了这个新选项,所见即所得。不过,当涉及到模块互操作时,这确实有一些影响。在此标志下,当设置或文件扩展名暗示不同的模块系统时,ECMAScript 导入和导出不会被重写为 require 调用。相反,会得到一个错误。如果需要生成使用 require 和 module.exports 的代码,则必须使用早于 ES2015 的 TypeScript 模块语法:
input typescript | output javascript |
---|---|
import foo = require(“foo”); | const foo = require(“foo”); |
function foo() {} function bar() {} function baz() {} export = { foo, bar, baz }; | function foo() {} function bar() {} function baz() {} module.export = { foo, bar, baz }; |
虽然这是一个限制,但它确实有助于使一些问题更加明显。例如,忘记在 --module node16 下的 package.json 中设置 type 字段是很常见的。因此,开发人员会在没有意识到的情况下开始编写 CommonJS 模块而不是 ES 模块,从而给出意外的查找规则和 JavaScript 输出。这个新标志确保有意使用正在使用的文件类型,因为语法是有意不同的。
因为 --verbatimModuleSyntax 提供了比 --importsNotUsedAsValues 和 --preserveValueImports 更一致的作用,所以这两个现有标志被弃用了。
6.2.8 支持 export type *
当 TypeScript 3.8 引入仅类型导入时,新语法不允许在 export * from “module” 或 export * as ns from “module” 重新导出时使用。TypeScript 5.0 添加了对这两种形式的支持:
// models/vehicles.ts
export class Spaceship {
// ...
}
// models/index.ts
export type * as vehicles from "./vehicles";
// main.ts
import { vehicles } from "./models";
function takeASpaceship(s: vehicles.Spaceship) {
// ✅
}
function makeASpaceship() {
return new vehicles.Spaceship();
// ^^^^^^^^
// vehicles 不能用作值,因为它是使用“export type”导出的。
}
6.2.9 JSDoc 支持 @satisfies
TypeScript 4.9 引入了 satisfies 操作符。它确保表达式的类型是兼容的,而不影响类型本身。以下面的代码为例:
interface CompilerOptions {
strict?: boolean;
outDir?: string;
// ...
}
interface ConfigSettings {
compilerOptions?: CompilerOptions;
extends?: string | string[];
// ...
}
let myConfigSettings = {
compilerOptions: {
strict: true,
outDir: "../lib",
// ...
},
extends: [
"@tsconfig/strictest/tsconfig.json",
"../../../tsconfig.base.json"
],
} satisfies ConfigSettings;
这里,TypeScript 知道 myCompilerOptions.extends 是用数组声明的,因为虽然 satisfies 验证了对象的类型,但它并没有直接将其更改为 CompilerOptions 而丢失信息。所以如果想映射到 extends 上,是可以的。
declare function resolveConfig(configPath: string): CompilerOptions;
let inheritedConfigs = myConfigSettings.extends.map(resolveConfig);
这对 TypeScript 用户很有帮助,但是很多人使用 TypeScript 来使用 JSDoc 注释对 JavaScript 代码进行类型检查。这就是为什么 TypeScript 5.0 支持一个名为 @satisfies 的新 JSDoc 标签,它做的事情完全一样。
/** @satisfies */ 可以捕获类型不匹配:
// @ts-check
/**
* @typedef CompilerOptions
* @prop {boolean} [strict]
* @prop {string} [outDir]
*/
/**
* @satisfies {CompilerOptions}
*/
let myCompilerOptions = {
outdir: "../lib",
// ~~~~~~ oops! we meant outDir
};
但它会保留表达式的原始类型,允许稍后在代码中更精确地使用值。
// @ts-check
/**
* @typedef CompilerOptions
* @prop {boolean} [strict]
* @prop {string} [outDir]
*/
/**
* @typedef ConfigSettings
* @prop {CompilerOptions} [compilerOptions]
* @prop {string | string[]} [extends]
*/
/**
* @satisfies {ConfigSettings}
*/
let myConfigSettings = {
compilerOptions: {
strict: true,
outDir: "../lib",
},
extends: [
"@tsconfig/strictest/tsconfig.json",
"../../../tsconfig.base.json"
],
};
let inheritedConfigs = myConfigSettings.extends.map(resolveConfig);
/** @satisfies */ 也可以内嵌在任何带括号的表达式上。可以这样写 myCompilerOptions:
let myConfigSettings = /** @satisfies {ConfigSettings} */ ({
compilerOptions: {
strict: true,
outDir: "../lib",
},
extends: [
"@tsconfig/strictest/tsconfig.json",
"../../../tsconfig.base.json"
],
});
这可能在函数调用时更有意义:
compileCode(/** @satisfies {CompilerOptions} */ ({
// ...
}));
6.2.10 JSDoc 支持 @overload
在 TypeScript 中,可以为函数指定重载。重载提供了一种方式,用不同的参数调用一个函数,并返回不同的结果。它可以限制调用者实际使用函数的方式,并优化将返回的结果。
// 重载:
function printValue(str: string): void;
function printValue(num: number, maxFractionDigits?: number): void;
// 实现:
function printValue(value: string | number, maximumFractionDigits?: number) {
if (typeof value === "number") {
const formatter = Intl.NumberFormat("en-US", {
maximumFractionDigits,
});
value = formatter.format(value);
}
console.log(value);
}
这里,printValue 将字符串或数字作为第一个参数。如果它需要一个数字,它可以使用第二个参数来确定可以打印多少个小数位。
TypeScript 5.0 现在允许 JSDoc 使用新的 @overload 标签声明重载。每个带有 @overload标签的 JSDoc 注释都被视为以下函数声明的不同重载。
// @ts-check
/**
* @overload
* @param {string} value
* @return {void}
*/
/**
* @overload
* @param {number} value
* @param {number} [maximumFractionDigits]
* @return {void}
*/
/**
* @param {string | number} value
* @param {number} [maximumFractionDigits]
*/
function printValue(value, maximumFractionDigits) {
if (typeof value === "number") {
const formatter = Intl.NumberFormat("en-US", {
maximumFractionDigits,
});
value = formatter.format(value);
}
console.log(value);
}
现在,无论是在 TypeScript 还是 JavaScript 文件中编写,TypeScript 都可以让我们知道是否错误地调用了函数。
printValue("hello!");
printValue(123.45);
printValue(123.45, 2);
printValue("hello!", 123); // ❌
6.2.11 编辑器中不区分大小写的导入排序
在 Visual Studio 和 VS Code 等编辑器中,TypeScript 支持组织和排序导入和导出的体验。但是,对于列表何时“排序”,通常会有不同的解释。
例如,下面的导入列表是否排序?
import {
Toggle,
freeze,
toBoolean,
} from "./utils";
答案可能是“视情况而定”。如果不关心区分大小写,那么这个列表显然没有排序。字母 f 出现在 t 和 T 之前。
但在大多数编程语言中,排序默认是比较字符串的字节值。JavaScript 比较字符串的方式意味着“Toggle”总是在“freeze”之前,因为根据 ASCII 字符编码,大写字母在小写字母之前。所以从这个角度来看,导入列表是已排序的。
TypeScript 之前认为导入列表是已排序的,因为它会做基本的区分大小写的排序。对于喜欢不区分大小写排序的开发人员,或者使用像 ESLint 这样默认需要不区分大小写排序的工具的开发人员来说,这可能是一个阻碍。
TypeScript 现在默认检测大小写。这意味着 TypeScript 和 ESLint 等工具通常不会就如何最好地对导入进行排序而相互“斗争”。
这些选项最终可能由编辑器配置。目前,它们仍然不稳定且处于试验阶段,现在可以通过在 JSON 选项中使用 typescript.unstable 在 VS Code 中选择加入它们。以下是可以尝试的所有选项(设置为默认值):
{
"typescript.unstable": {
// Should sorting be case-sensitive? Can be:
// - true
// - false
// - "auto" (auto-detect)
"organizeImportsIgnoreCase": "auto",
// Should sorting be "ordinal" and use code points or consider Unicode rules? Can be:
// - "ordinal"
// - "unicode"
"organizeImportsCollation": "ordinal",
// Under `"organizeImportsCollation": "unicode"`,
// what is the current locale? Can be:
// - [any other locale code]
// - "auto" (use the editor's locale)
"organizeImportsLocale": "en",
// Under `"organizeImportsCollation": "unicode"`,
// should upper-case letters or lower-case letters come first? Can be:
// - false (locale-specific)
// - "upper"
// - "lower"
"organizeImportsCaseFirst": false,
// Under `"organizeImportsCollation": "unicode"`,
// do runs of numbers get compared numerically (i.e. "a1" < "a2" < "a100")? Can be:
// - true
// - false
"organizeImportsNumericCollation": true,
// Under `"organizeImportsCollation": "unicode"`,
// do letters with accent marks/diacritics get sorted distinctly
// from their "base" letter (i.e. is é different from e)? Can be
// - true
// - false
"organizeImportsAccentCollation": true
},
"javascript.unstable": {
// same options valid here...
},
}
6.2.12 完善 switch/case
在编写 switch 语句时,TypeScript 现在会检测被检查的值何时具有字面量类型。以提供更便利的代码快捷输入:
6.2.13 优化速度、内存和包大小
TypeScript 5.0 在代码结构、数据结构和算法实现中包含许多强大的变化。这些都意味着整个体验应该更快——不仅仅是运行 TypeScript,甚至安装它。
以下是相对于 TypeScript 4.9 在速度和大小方面的优势:
场景 | 时间或大小相对于 TS 4.9 |
---|---|
material-ui 构建时间 | 90% |
typescript 编译器启动时间 | 89% |
playwright 构建时间 | 88% |
typescript 编译器自构建时间 | 87% |
Outlook web 构建时间 | 82% |
VS code 构建时间 | 80% |
typescript npm 包大小 | 59% |
TypeScript 包体积变化:
TypeScript4.9 | TypeScript5.0 |
---|---|
63.8 MB | 37.4 MB |
那为什么会有如此大的提升呢?部分优化细节如下:
首先,将 TypeScript 从命名空间迁移到模块,这样就能够利用现代构建工具来执行优化。重新审视了打包策略并删除一些已弃用的代码,已将 TypeScript 4.9 的 63.8 MB 包大小减少了约 26.4 MB。还通过直接函数调用带来了显著的速度提升。
在将信息序列化为字符串时,执行了一些缓存。类型显示可能作为错误报告、声明触发、代码补全等的一部分发生,最终可能会相当昂贵。TypeScript 现在缓存了一些常用的机制以在这些操作中重用。预计大多数代码库应该会看到 TypeScript 5.0 的速度提升,并且始终能够重现 10% 到 20% 之间的提升。当然,这将取决于硬件和代码库特性。
6.2.14 其他重大更改和弃用
运行时要求
TypeScript 现在的 target 是 ECMAScript 2018。TypeScript 软件包还将预期的最低引擎版本设置为 12.20。对于 Node.js 用户来说,这意味着 TypeScript 5.0 需要至少Node.js 12.20 或更高版本才能运行。
lib.d.ts 变化
更改 DOM 类型的生成方式可能会对现有代码产生影响。注意,某些属性已从数字转换为数字字面量类型,并且用于剪切、复制和粘贴事件处理的属性和方法已跨接口移动。
API 重大变更
在 TypeScript 5.0 中, 转向了模块,删除了一些不必要的接口,并进行了一些正确性改进。
关系运算符中的禁止隐式强制
如果编写的代码可能导致隐式字符串到数字的强制转换,TypeScript 中的某些操作现在会进行警告:
function func(ns: number | string) {
return ns * 4; // 错误,可能存在隐式强制转换
}
在 5.0 中,这也将应用于关系运算符 >、<、<= 和 >=:
function func(ns: number | string) {
return ns > 4;
}
如果需要这样做,可以使用+显式地将操作数转换为数字:
function func(ns: number | string) {
return +ns > 4; // OK
}
弃用和默认更改
在 TypeScript 5.0 中,弃用了以下设置和设置值:
–target: ES3
–out
–noImplicitUseStrict
–keyofStringsOnly
–suppressExcessPropertyErrors
–suppressImplicitAnyIndexErrors
–noStrictGenericChecks
–charset
–importsNotUsedAsValues
–preserveValueImports
在 TypeScript 5.5 之前,这些配置将继续被允许使用,届时它们将被完全删除,但是,如果正在使用这些设置,将收到警告。在 TypeScript 5.0 以及未来版本 5.1、5.2、5.3 和 5.4 中,可以指定 “ignoreDeprecations”: “5.0” 以消除这些警告。TypeScript 团队很快会发布一个 4.9 补丁,允许指定 ignoreDeprecations 以实现更平滑的升级。除了弃用之外,还更改了一些设置以更好地改进 TypeScript 中的跨平台行为。
- –newLine,控制 JavaScript 文件中发出的行结束符,如果没有指定,过去是根据当前操作系统推断的。我们认为构建应该尽可能确定,Windows 记事本现在支持换行符,所以新的默认设置是 LF。旧的特定于操作系统的推理行为不再可用。
- –forceConsistentCasingInFileNames,它确保项目中对相同文件名的所有引用都在大小写中达成一致,现在默认为 true。这有助于捕获在不区分大小写的文件系统上编写的代码的差异问题。
7. 5分钟用TypeScript创建一个简单的Web应用
让我们使用TypeScript来创建一个简单的Web应用。
7.1 安装TypeScript
有两种主要的方式来获取TypeScript工具:
通过npm(Node.js包管理器)
安装Visual Studio的TypeScript插件
Visual Studio 2017和Visual Studio 2015 Update 3默认包含了TypeScript。 如果你的Visual Studio还没有安装TypeScript,你可以下载它。
针对使用npm的用户:
> npm install -g typescript
7.2 构建你的第一个TypeScript文件
在编辑器,将下面的代码输入到greeter.ts文件里:
function greeter(person) {
return "Hello, " + person;
}
let user = "Jane User";
document.body.innerHTML = greeter(user);
7.3 编译代码
我们使用了.ts扩展名,但是这段代码仅仅是JavaScript而已。 你可以直接从现有的JavaScript应用里复制/粘贴这段代码。
在命令行上,运行TypeScript编译器:
tsc greeter.ts
输出结果为一个greeter.js文件,它包含了和输入文件中相同的JavsScript代码。 一切准备就绪,我们可以运行这个使用TypeScript写的JavaScript应用了!
接下来让我们看看TypeScript工具带来的高级功能。 给 person函数的参数添加: string类型注解,如下:
function greeter(person: string) {
return "Hello, " + person;
}
let user = "Jane User";
document.body.innerHTML = greeter(user);
7.4 类型注解
TypeScript里的类型注解是一种轻量级的为函数或变量添加约束的方式。 在这个例子里,我们希望 greeter函数接收一个字符串参数。 然后尝试把 greeter的调用改成传入一个数组:
function greeter(person: string) {
return "Hello, " + person;
}
let user = [0, 1, 2];
document.body.innerHTML = greeter(user);
重新编译,你会看到产生了一个错误。
greeter.ts(7,26): error TS2345: Argument of type 'number[]' is not assignable to parameter of type 'string'.
类似地,尝试删除greeter调用的所有参数。 TypeScript会告诉你使用了非期望个数的参数调用了这个函数。 在这两种情况中,TypeScript提供了静态的代码分析,它可以分析代码结构和提供的类型注解。
要注意的是尽管有错误,greeter.js文件还是被创建了。 就算你的代码里有错误,你仍然可以使用TypeScript。但在这种情况下,TypeScript会警告你代码可能不会按预期执行。
7.5 接口
让我们开发这个示例应用。这里我们使用接口来描述一个拥有firstName和lastName字段的对象。 在TypeScript里,只在两个类型内部的结构兼容那么这两个类型就是兼容的。 这就允许我们在实现接口时候只要保证包含了接口要求的结构就可以,而不必明确地使用 implements语句。
interface Person {
firstName: string;
lastName: string;
}
function greeter(person: Person) {
return "Hello, " + person.firstName + " " + person.lastName;
}
let user = { firstName: "Jane", lastName: "User" };
document.body.innerHTML = greeter(user);
7.6 类
最后,让我们使用类来改写这个例子。 TypeScript支持JavaScript的新特性,比如支持基于类的面向对象编程。
让我们创建一个Student类,它带有一个构造函数和一些公共字段。 注意类和接口可以一起共作,程序员可以自行决定抽象的级别。
还要注意的是,在构造函数的参数上使用public等同于创建了同名的成员变量。
class Student {
fullName: string;
constructor(public firstName, public middleInitial, public lastName) {
this.fullName = firstName + " " + middleInitial + " " + lastName;
}
}
interface Person {
firstName: string;
lastName: string;
}
function greeter(person : Person) {
return "Hello, " + person.firstName + " " + person.lastName;
}
let user = new Student("Jane", "M.", "User");
document.body.innerHTML = greeter(user);
重新运行tsc greeter.ts,你会看到生成的JavaScript代码和原先的一样。 TypeScript里的类只是JavaScript里常用的基于原型面向对象编程的简写。
7.7 运行TypeScript Web应用
在greeter.html里输入如下内容:
<!DOCTYPE html>
<html>
<head><title>TypeScript Greeter</title></head>
<body>
<script src="greeter.js"></script>
</body>
</html>
在浏览器里打开greeter.html运行这个应用!
可选地:在Visual Studio里打开greeter.ts或者把代码复制到TypeScript playground。 将鼠标悬停在标识符上查看它们的类型。 注意在某些情况下它们的类型可以被自动地推断出来。 重新输入一下最后一行代码,看一下自动补全列表和参数列表,它们会根据DOM元素类型而变化。 将光标放在 greeter函数上,点击F12可以跟踪到它的定义。 还有一点,你可以右键点击标识,使用重构功能来重命名。
这些类型信息以及工具可以很好的和JavaScript一起工作。
8.TypeScript基础类型
8.1 TypeScript布尔值
最基本的数据类型就是简单的true/false值,在JavaScript和TypeScript里叫做boolean(其它语言中也一样)。
let isDone: boolean = false;
8.2 TypeScript数字
和JavaScript一样,TypeScript里的所有数字都是浮点数。 这些浮点数的类型是 number。 除了支持十进制和十六进制字面量,TypeScript还支持ECMAScript 2015中引入的二进制和八进制字面量。
let decLiteral: number = 6;
let hexLiteral: number = 0xf00d;
let binaryLiteral: number = 0b1010;
let octalLiteral: number = 0o744;
8.3 TypeScript字符串
JavaScript程序的另一项基本操作是处理网页或服务器端的文本数据。 像其它语言里一样,我们使用 string表示文本数据类型。 和JavaScript一样,可以使用双引号( ")或单引号(')表示字符串。
let name: string = "bob";
name = "smith";
你还可以使用模版字符串,它可以定义多行文本和内嵌表达式。 这种字符串是被反引号包围( `),并且以${ expr }这种形式嵌入表达式
let name: string = `Gene`;
let age: number = 37;
let sentence: string = `Hello, my name is ${ name }.
I'll be ${ age + 1 } years old next month.`;
这与下面定义sentence的方式效果相同:
let sentence: string = "Hello, my name is " + name + ".\n\n" +
"I'll be " + (age + 1) + " years old next month.";
```
### 8.4 TypeScript数组
TypeScript像JavaScript一样可以操作数组元素。 有两种方式可以定义数组。 第一种,可以在元素类型后面接上 [],表示由此类型元素组成的一个数组:
```typescript
let list: number[] = [1, 2, 3];
第二种方式是使用数组泛型,Array<元素类型>:
let list: Array<number> = [1, 2, 3];
8.5 TypeScript元组 Tuple
元组类型允许表示一个已知元素数量和类型的数组,各元素的类型不必相同。 比如,你可以定义一对值分别为 string和number类型的元组。
// Declare a tuple type
let x: [string, number];
// Initialize it
x = ['hello', 10]; // OK
// Initialize it incorrectly
x = [10, 'hello']; // Error
当访问一个已知索引的元素,会得到正确的类型:
console.log(x[0].substr(1)); // OK
console.log(x[1].substr(1)); // Error, 'number' does not have 'substr'
当访问一个越界的元素,会使用联合类型替代:
x[3] = 'world'; // OK, 字符串可以赋值给(string | number)类型
console.log(x[5].toString()); // OK, 'string' 和 'number' 都有 toString
x[6] = true; // Error, 布尔不是(string | number)类型
联合类型是高级主题,我们会在以后的章节里讨论它。
8.6 TypeScript枚举
enum类型是对JavaScript标准数据类型的一个补充。 像C#等其它语言一样,使用枚举类型可以为一组数值赋予友好的名字。
enum Color {Red, Green, Blue}
let c: Color = Color.Green;
默认情况下,从0开始为元素编号。 你也可以手动的指定成员的数值。 例如,我们将上面的例子改成从 1开始编号:
enum Color {Red = 1, Green, Blue}
let c: Color = Color.Green;
或者,全部都采用手动赋值:
enum Color {Red = 1, Green = 2, Blue = 4}
let c: Color = Color.Green;
枚举类型提供的一个便利是你可以由枚举的值得到它的名字。 例如,我们知道数值为2,但是不确定它映射到Color里的哪个名字,我们可以查找相应的名字:
enum Color {Red = 1, Green, Blue}
let colorName: string = Color[2];
console.log(colorName); // 显示'Green'因为上面代码里它的值是2
8.7 TypeScript的Any
有时候,我们会想要为那些在编程阶段还不清楚类型的变量指定一个类型。 这些值可能来自于动态的内容,比如来自用户输入或第三方代码库。 这种情况下,我们不希望类型检查器对这些值进行检查而是直接让它们通过编译阶段的检查。 那么我们可以使用 any类型来标记这些变量:
let notSure: any = 4;
notSure = "maybe a string instead";
notSure = false; // okay, definitely a boolean
在对现有代码进行改写的时候,any类型是十分有用的,它允许你在编译时可选择地包含或移除类型检查。 你可能认为 Object有相似的作用,就像它在其它语言中那样。 但是 Object类型的变量只是允许你给它赋任意值 - 但是却不能够在它上面调用任意的方法,即便它真的有这些方法:
let notSure: any = 4;
notSure.ifItExists(); // okay, ifItExists might exist at runtime
notSure.toFixed(); // okay, toFixed exists (but the compiler doesn't check)
let prettySure: Object = 4;
prettySure.toFixed(); // Error: Property 'toFixed' doesn't exist on type 'Object'.
当你只知道一部分数据的类型时,any类型也是有用的。 比如,你有一个数组,它包含了不同的类型的数据:
let list: any[] = [1, true, "free"];
list[1] = 100;
8.8 TypeScript的Void
某种程度上来说,void类型像是与any类型相反,它表示没有任何类型。 当一个函数没有返回值时,你通常会见到其返回值类型是 void:
function warnUser(): void {
console.log("This is my warning message");
}
声明一个void类型的变量没有什么大用,因为你只能为它赋予undefined和null:
let unusable: void = undefined;
8.9 TypeScript的Null和Undefined
TypeScript里,undefined和null两者各自有自己的类型分别叫做undefined和null。 和 void相似,它们的本身的类型用处不是很大:
// Not much else we can assign to these variables!
let u: undefined = undefined;
let n: null = null;
默认情况下null和undefined是所有类型的子类型。 就是说你可以把 null和undefined赋值给number类型的变量。
然而,当你指定了–strictNullChecks标记,null和undefined只能赋值给void和它们各自。 这能避免 很多常见的问题。 也许在某处你想传入一个 string或null或undefined,你可以使用联合类型string | null | undefined。 再次说明,稍后我们会介绍联合类型。
注意:我们鼓励尽可能地使用–strictNullChecks,但在本手册里我们假设这个标记是关闭的。
8.10 TypeScript的Never
never类型表示的是那些永不存在的值的类型。 例如, never类型是那些总是会抛出异常或根本就不会有返回值的函数表达式或箭头函数表达式的返回值类型; 变量也可能是 never类型,当它们被永不为真的类型保护所约束时。
never类型是任何类型的子类型,也可以赋值给任何类型;然而,没有类型是never的子类型或可以赋值给never类型(除了never本身之外)。 即使 any也不可以赋值给never。
下面是一些返回never类型的函数:
// 返回never的函数必须存在无法达到的终点
function error(message: string): never {
throw new Error(message);
}
// 推断的返回值类型为never
function fail() {
return error("Something failed");
}
// 返回never的函数必须存在无法达到的终点
function infiniteLoop(): never {
while (true) {
}
}
8.11 TypeScript的Object
object表示非原始类型,也就是除number,string,boolean,symbol,null或undefined之外的类型。
使用object类型,就可以更好的表示像Object.create这样的API。例如:
declare function create(o: object | null): void;
create({ prop: 0 }); // OK
create(null); // OK
create(42); // Error
create("string"); // Error
create(false); // Error
create(undefined); // Error
8.12 TypeScript类型断言
有时候你会遇到这样的情况,你会比TypeScript更了解某个值的详细信息。 通常这会发生在你清楚地知道一个实体具有比它现有类型更确切的类型。
通过类型断言这种方式可以告诉编译器,“相信我,我知道自己在干什么”。 类型断言好比其它语言里的类型转换,但是不进行特殊的数据检查和解构。 它没有运行时的影响,只是在编译阶段起作用。 TypeScript会假设你,程序员,已经进行了必须的检查。
类型断言有两种形式。 其一是“尖括号”语法:
let someValue: any = "this is a string";
let strLength: number = (<string>someValue).length;
另一个为as语法:
let someValue: any = "this is a string";
let strLength: number = (someValue as string).length;
两种形式是等价的。 至于使用哪个大多数情况下是凭个人喜好;然而,当你在TypeScript里使用JSX时,只有 as语法断言是被允许的。
关于let
你可能已经注意到了,我们使用let关键字来代替大家所熟悉的JavaScript关键字var。 let关键字是JavaScript的一个新概念,TypeScript实现了它。 我们会在以后详细介绍它,很多常见的问题都可以通过使用 let来解决,所以尽可能地使用let来代替var吧。
9.TypeScript变量声明
let和const是JavaScript里相对较新的变量声明方式。 像我们之前提到过的, let在很多方面与var是相似的,但是可以帮助大家避免在JavaScript里常见一些问题。 const是对let的一个增强,它能阻止对一个变量再次赋值。
因为TypeScript是JavaScript的超集,所以它本身就支持let和const。 下面我们会详细说明这些新的声明方式以及为什么推荐使用它们来代替 var。
如果你之前使用JavaScript时没有特别在意,那么这节内容会唤起你的回忆。 如果你已经对 var声明的怪异之处了如指掌,那么你可以轻松地略过这节。
9.1 var 声明
一直以来我们都是通过var关键字定义JavaScript变量。
var a = 10;
大家都能理解,这里定义了一个名为a值为10的变量。
我们也可以在函数内部定义变量:
function f() {
var message = "Hello, world!";
return message;
}
并且我们也可以在其它函数内部访问相同的变量。
function f() {
var a = 10;
return function g() {
var b = a + 1;
return b;
}
}
var g = f();
g(); // returns 11;
上面的例子里,g可以获取到f函数里定义的a变量。 每当 g被调用时,它都可以访问到f里的a变量。 即使当 g在f已经执行完后才被调用,它仍然可以访问及修改a。
function f() {
var a = 1;
a = 2;
var b = g();
a = 3;
return b;
function g() {
return a;
}
}
f(); // returns 2
9.2 作用域规则
对于熟悉其它语言的人来说,var声明有些奇怪的作用域规则。 看下面的例子:
function f(shouldInitialize: boolean) {
if (shouldInitialize) {
var x = 10;
}
return x;
}
f(true); // returns '10'
f(false); // returns 'undefined'
有些读者可能要多看几遍这个例子。 变量 x是定义在if语句里面,但是我们却可以在语句的外面访问它。 这是因为 var声明可以在包含它的函数,模块,命名空间或全局作用域内部任何位置被访问(我们后面会详细介绍),包含它的代码块对此没有什么影响。 有些人称此为* var作用域或函数作用域*。 函数参数也使用函数作用域。
这些作用域规则可能会引发一些错误。 其中之一就是,多次声明同一个变量并不会报错:
function sumMatrix(matrix: number[][]) {
var sum = 0;
for (var i = 0; i < matrix.length; i++) {
var currentRow = matrix[i];
for (var i = 0; i < currentRow.length; i++) {
sum += currentRow[i];
}
}
return sum;
}
这里很容易看出一些问题,里层的for循环会覆盖变量i,因为所有i都引用相同的函数作用域内的变量。 有经验的开发者们很清楚,这些问题可能在代码审查时漏掉,引发无穷的麻烦。
9.3 捕获变量怪异之处
快速的猜一下下面的代码会返回什么:
for (var i = 0; i < 10; i++) {
setTimeout(function() { console.log(i); }, 100 * i);
}
介绍一下,setTimeout会在若干毫秒的延时后执行一个函数(等待其它代码执行完毕)。
好吧,看一下结果:
10
10
10
10
10
10
10
10
10
10
很多JavaScript程序员对这种行为已经很熟悉了,但如果你很不解,你并不是一个人。 大多数人期望输出结果是这样:
0
1
2
3
4
5
6
7
8
9
还记得我们上面提到的捕获变量吗?
我们传给setTimeout的每一个函数表达式实际上都引用了相同作用域里的同一个i。
让我们花点时间思考一下这是为什么。 setTimeout在若干毫秒后执行一个函数,并且是在for循环结束后。 for循环结束后,i的值为10。 所以当函数被调用的时候,它会打印出 10!
一个通常的解决方法是使用立即执行的函数表达式(IIFE)来捕获每次迭代时i的值:
for (var i = 0; i < 10; i++) {
// capture the current state of 'i'
// by invoking a function with its current value
(function(i) {
setTimeout(function() { console.log(i); }, 100 * i);
})(i);
}
这种奇怪的形式我们已经司空见惯了。 参数 i会覆盖for循环里的i,但是因为我们起了同样的名字,所以我们不用怎么改for循环体里的代码。
9.4 let 声明
现在你已经知道了var存在一些问题,这恰好说明了为什么用let语句来声明变量。 除了名字不同外, let与var的写法一致。
let hello = "Hello!";
主要的区别不在语法上,而是语义,我们接下来会深入研究。
9.5 块作用域
当用let声明一个变量,它使用的是词法作用域或块作用域。 不同于使用 var声明的变量那样可以在包含它们的函数外访问,块作用域变量在包含它们的块或for循环之外是不能访问的。
function f(input: boolean) {
let a = 100;
if (input) {
// Still okay to reference 'a'
let b = a + 1;
return b;
}
// Error: 'b' doesn't exist here
return b;
}
这里我们定义了2个变量a和b。 a的作用域是f函数体内,而b的作用域是if语句块里。
在catch语句里声明的变量也具有同样的作用域规则。
try {
throw "oh no!";
}
catch (e) {
console.log("Oh well.");
}
// Error: 'e' doesn't exist here
console.log(e);
拥有块级作用域的变量的另一个特点是,它们不能在被声明之前读或写。 虽然这些变量始终“存在”于它们的作用域里,但在直到声明它的代码之前的区域都属于 暂时性死区。 它只是用来说明我们不能在 let语句之前访问它们,幸运的是TypeScript可以告诉我们这些信息。
a++; // illegal to use 'a' before it's declared;
let a;
注意一点,我们仍然可以在一个拥有块作用域变量被声明前获取它。 只是我们不能在变量声明前去调用那个函数。 如果生成代码目标为ES2015,现代的运行时会抛出一个错误;然而,现今TypeScript是不会报错的。
function foo() {
// okay to capture 'a'
return a;
}
// 不能在'a'被声明前调用'foo'
// 运行时应该抛出错误
foo();
let a;
关于暂时性死区的更多信息,查看这里Mozilla Developer Network.
9.6 重定义及屏蔽
我们提过使用var声明时,它不在乎你声明多少次;你只会得到1个。
function f(x) {
var x;
var x;
if (true) {
var x;
}
}
在上面的例子里,所有x的声明实际上都引用一个相同的x,并且这是完全有效的代码。 这经常会成为bug的来源。 好的是, let声明就不会这么宽松了。
let x = 10;
let x = 20; // 错误,不能在1个作用域里多次声明`x`
并不是要求两个均是块级作用域的声明TypeScript才会给出一个错误的警告。
function f(x) {
let x = 100; // error: interferes with parameter declaration
}
function g() {
let x = 100;
var x = 100; // error: can't have both declarations of 'x'
}
并不是说块级作用域变量不能用函数作用域变量来声明。 而是块级作用域变量需要在明显不同的块里声明。
function f(condition, x) {
if (condition) {
let x = 100;
return x;
}
return x;
}
f(false, 0); // returns 0
f(true, 0); // returns 100
在一个嵌套作用域里引入一个新名字的行为称做屏蔽。 它是一把双刃剑,它可能会不小心地引入新问题,同时也可能会解决一些错误。 例如,假设我们现在用 let重写之前的sumMatrix函数。
function sumMatrix(matrix: number[][]) {
let sum = 0;
for (let i = 0; i < matrix.length; i++) {
var currentRow = matrix[i];
for (let i = 0; i < currentRow.length; i++) {
sum += currentRow[i];
}
}
return sum;
}
这个版本的循环能得到正确的结果,因为内层循环的i可以屏蔽掉外层循环的i。
通常来讲应该避免使用屏蔽,因为我们需要写出清晰的代码。 同时也有些场景适合利用它,你需要好好打算一下。
9.7 块级作用域变量的获取
在我们最初谈及获取用var声明的变量时,我们简略地探究了一下在获取到了变量之后它的行为是怎样的。 直观地讲,每次进入一个作用域时,它创建了一个变量的 环境。 就算作用域内代码已经执行完毕,这个环境与其捕获的变量依然存在。
function theCityThatAlwaysSleeps() {
let getCity;
if (true) {
let city = "Seattle";
getCity = function() {
return city;
}
}
return getCity();
}
因为我们已经在city的环境里获取到了city,所以就算if语句执行结束后我们仍然可以访问它。
回想一下前面setTimeout的例子,我们最后需要使用立即执行的函数表达式来获取每次for循环迭代里的状态。 实际上,我们做的是为获取到的变量创建了一个新的变量环境。 这样做挺痛苦的,但是幸运的是,你不必在TypeScript里这样做了。
当let声明出现在循环体里时拥有完全不同的行为。 不仅是在循环里引入了一个新的变量环境,而是针对 每次迭代都会创建这样一个新作用域。 这就是我们在使用立即执行的函数表达式时做的事,所以在 setTimeout例子里我们仅使用let声明就可以了。
for (let i = 0; i < 10 ; i++) {
setTimeout(function() {console.log(i); }, 100 * i);
}
会输出与预料一致的结果:
0
1
2
3
4
5
6
7
8
9
9.8 const 声明
const 声明是声明变量的另一种方式。
const numLivesForCat = 9;
它们与let声明相似,但是就像它的名字所表达的,它们被赋值后不能再改变。 换句话说,它们拥有与 let相同的作用域规则,但是不能对它们重新赋值。
这很好理解,它们引用的值是不可变的。
const numLivesForCat = 9;
const kitty = {
name: "Aurora",
numLives: numLivesForCat,
}
// Error
kitty = {
name: "Danielle",
numLives: numLivesForCat
};
// all "okay"
kitty.name = "Rory";
kitty.name = "Kitty";
kitty.name = "Cat";
kitty.numLives--;
除非你使用特殊的方法去避免,实际上const变量的内部状态是可修改的。 幸运的是,TypeScript允许你将对象的成员设置成只读的。 接口一章有详细说明。
9.9 let vs. const
现在我们有两种作用域相似的声明方式,我们自然会问到底应该使用哪个。 与大多数泛泛的问题一样,答案是:依情况而定。
使用最小特权原则,所有变量除了你计划去修改的都应该使用const。 基本原则就是如果一个变量不需要对它写入,那么其它使用这些代码的人也不能够写入它们,并且要思考为什么会需要对这些变量重新赋值。 使用 const也可以让我们更容易的推测数据的流动。
跟据你的自己判断,如果合适的话,与团队成员商议一下。
这个手册大部分地方都使用了let声明。
9.10 解构
Another TypeScript已经可以解析其它 ECMAScript 2015 特性了。 完整列表请参见 the article on the Mozilla Developer Network。 本章,我们将给出一个简短的概述。
9.10.1 解构数组
最简单的解构莫过于数组的解构赋值了:
let input = [1, 2];
let [first, second] = input;
console.log(first); // outputs 1
console.log(second); // outputs 2
这创建了2个命名变量 first 和 second。 相当于使用了索引,但更为方便:
first = input[0];
second = input[1];
解构作用于已声明的变量会更好:
// swap variables
[first, second] = [second, first];
作用于函数参数:
function f([first, second]: [number, number]) {
console.log(first);
console.log(second);
}
f(input);
你可以在数组里使用…语法创建剩余变量:
let [first, ...rest] = [1, 2, 3, 4];
console.log(first); // outputs 1
console.log(rest); // outputs [ 2, 3, 4 ]
当然,由于是JavaScript, 你可以忽略你不关心的尾随元素:
let [first] = [1, 2, 3, 4];
console.log(first); // outputs 1
或其它元素:
let [, second, , fourth] = [1, 2, 3, 4];
9.10.2 对象解构
你也可以解构对象:
let o = {
a: "foo",
b: 12,
c: "bar"
};
let { a, b } = o;
这通过 o.a and o.b 创建了 a 和 b 。 注意,如果你不需要 c 你可以忽略它。
就像数组解构,你可以用没有声明的赋值:
({ a, b } = { a: "baz", b: 101 });
注意,我们需要用括号将它括起来,因为Javascript通常会将以 { 起始的语句解析为一个块。
你可以在对象里使用…语法创建剩余变量:
let { a, ...passthrough } = o;
let total = passthrough.b + passthrough.c.length;
属性重命名
你也可以给属性以不同的名字:
let { a: newName1, b: newName2 } = o;
这里的语法开始变得混乱。 你可以将 a: newName1 读做 “a 作为 newName1”。 方向是从左到右,好像你写成了以下样子:
let newName1 = o.a;
let newName2 = o.b;
令人困惑的是,这里的冒号不是指示类型的。 如果你想指定它的类型, 仍然需要在其后写上完整的模式。
let {a, b}: {a: string, b: number} = o;
默认值
默认值可以让你在属性为 undefined 时使用缺省值:
function keepWholeObject(wholeObject: { a: string, b?: number }) {
let { a, b = 1001 } = wholeObject;
}
现在,即使 b 为 undefined , keepWholeObject 函数的变量 wholeObject 的属性 a 和 b 都会有值。
9.11 函数声明
解构也能用于函数声明。 看以下简单的情况:
type C = { a: string, b?: number }
function f({ a, b }: C): void {
// ...
}
但是,通常情况下更多的是指定默认值,解构默认值有些棘手。 首先,你需要在默认值之前设置其格式。
function f({ a="", b=0 } = {}): void {
// ...
}
f();
上面的代码是一个类型推断的例子,将在本手册后文介绍。
其次,你需要知道在解构属性上给予一个默认或可选的属性用来替换主初始化列表。 要知道 C 的定义有一个 b 可选属性:
function f({ a, b = 0 } = { a: "" }): void {
// ...
}
f({ a: "yes" }); // ok, default b = 0
f(); // ok, default to {a: ""}, which then defaults b = 0
f({}); // error, 'a' is required if you supply an argument
要小心使用解构。 从前面的例子可以看出,就算是最简单的解构表达式也是难以理解的。 尤其当存在深层嵌套解构的时候,就算这时没有堆叠在一起的重命名,默认值和类型注解,也是令人难以理解的。 解构表达式要尽量保持小而简单。 你自己也可以直接使用解构将会生成的赋值表达式。
9.12 展开
展开操作符正与解构相反。 它允许你将一个数组展开为另一个数组,或将一个对象展开为另一个对象。 例如:
let first = [1, 2];
let second = [3, 4];
let bothPlus = [0, ...first, ...second, 5];
这会令bothPlus的值为[0, 1, 2, 3, 4, 5]。 展开操作创建了 first和second的一份浅拷贝。 它们不会被展开操作所改变。
你还可以展开对象:
let defaults = { food: "spicy", price: "$$", ambiance: "noisy" };
let search = { ...defaults, food: "rich" };
search的值为{ food: “rich”, price: “$$”, ambiance: “noisy” }。 对象的展开比数组的展开要复杂的多。 像数组展开一样,它是从左至右进行处理,但结果仍为对象。 这就意味着出现在展开对象后面的属性会覆盖前面的属性。 因此,如果我们修改上面的例子,在结尾处进行展开的话:
let defaults = { food: "spicy", price: "$$", ambiance: "noisy" };
let search = { food: "rich", ...defaults };
那么,defaults里的food属性会重写food: “rich”,在这里这并不是我们想要的结果。
对象展开还有其它一些意想不到的限制。 首先,它仅包含对象 自身的可枚举属性。 大体上是说当你展开一个对象实例时,你会丢失其方法:
class C {
p = 12;
m() {
}
}
let c = new C();
let clone = { ...c };
clone.p; // ok
clone.m(); // error!
其次,TypeScript编译器不允许展开泛型函数上的类型参数。 这个特性会在TypeScript的未来版本中考虑实现。
10. 接口
在TypeScript中,接口(Interface)是一种强大的类型系统特性,它定义了一组对象应该遵循的形状(shape)。接口可以定义对象的属性、方法以及它们的类型,确保实现接口的对象或类遵循这些规范。接口不仅可以描述对象的结构,还可以描述函数的类型,甚至可以用来定义类的类型。
接口的基本语法很简单,使用interface
关键字后跟接口名称,再在花括号中定义属性和方法。例如:
interface Person {
name: string;
age: number;
greet(): void;
}
在这个例子中,Person
接口定义了一个对象应该具有的name
(字符串类型)和age
(数字类型)属性,以及一个greet
方法(无参数,无返回值)。
10.1 接口与对象
一个对象如果具有与接口定义相匹配的属性和方法,那么这个对象就实现了该接口。TypeScript编译器会检查对象是否符合接口定义,如果不符合则会报错。
const john: Person = {
name: 'John Doe',
age: 30,
greet() {
console.log(`Hello, my name is ${this.name}.`);
}
};
在这个例子中,john
对象实现了Person
接口,因为它具有name
、age
属性和greet
方法。
10.2 接口与类
在面向对象的编程中,类是实现接口的主要方式。TypeScript允许类实现一个或多个接口,确保类遵循接口定义的结构。
class Employee implements Person {
name: string;
age: number;
position: string;
constructor(name: string, age: number, position: string) {
this.name = name;
this.age = age;
this.position = position;
}
greet() {
console.log(`Hello, I'm ${this.name} and I'm an ${this.position}.`);
}
}
在Employee
类中,我们实现了Person
接口,并且添加了一个额外的position
属性。类必须提供接口中定义的所有属性和方法的具体实现。
10.3 接口继承
接口之间也可以相互继承,这允许我们构建复杂的类型层次结构。
interface Musician extends Person {
instrument: string;
play(): void;
}
在这个例子中,Musician
接口继承了Person
接口,并添加了一个instrument
属性和一个play
方法。任何实现Musician
接口的对象或类都必须同时实现Person
接口和Musician
接口定义的所有属性和方法。
10.4 函数类型接口
接口不仅可以用来描述对象的结构,还可以用来定义函数的类型。
interface GreetingFunction {
(name: string): void;
}
const sayHello: GreetingFunction = (name) => {
console.log(`Hello, ${name}!`);
};
在这个例子中,我们定义了一个名为GreetingFunction
的接口,它描述了一个接受一个字符串参数并返回void
的函数类型。然后,我们创建了一个符合这个接口定义的sayHello
函数。
接口是TypeScript类型系统中非常强大和灵活的工具,它们允许我们定义复杂的数据结构和函数签名,确保代码的一致性和可维护性。通过接口,我们可以更好地组织和理解代码中的不同类型和它们之间的关系。
11. 类
类(Class)是面向对象编程(OOP)中的核心概念,TypeScript支持基于类的面向对象编程。类定义了对象的属性和方法,并允许创建这些对象的实例。通过类,可以创建具有相同属性和方法的对象集合,实现代码的重用和封装。
11.1 类的定义与实例化
在TypeScript中,类是通过class
关键字来定义的。类定义中可以包含属性(也称为字段或成员变量)和方法(也称为成员函数)。属性用于存储对象的状态信息,而方法则定义了对象的行为。
下面是一个简单的类定义示例:
class Person {
name: string;
age: number;
constructor(name: string, age: number) {
this.name = name;
this.age = age;
}
greet(): void {
console.log(`Hello, my name is ${this.name} and I'm ${this.age} years old.`);
}
}
在上面的例子中,Person
类有两个属性name
和age
,以及一个构造函数constructor
和一个方法greet
。构造函数用于初始化对象的属性,而greet
方法则用于输出一条问候信息。
要创建类的实例(即对象),可以使用new
关键字调用类的构造函数:
const person1 = new Person("Alice", 25);
person1.greet(); // 输出: Hello, my name is Alice and I'm 25 years old.
11.2 类的继承
TypeScript支持类的继承,允许一个类(子类或派生类)继承另一个类(父类或基类)的属性和方法。通过继承,子类可以重用父类的代码,并且可以添加或覆盖父类的方法,以实现更具体的行为。
下面是一个继承的示例:
class Employee extends Person {
position: string;
constructor(name: string, age: number, position: string) {
super(name, age); // 调用父类的构造函数
this.position = position;
}
greet(): void {
super.greet(); // 调用父类的greet方法
console.log(`I work as a ${this.position}.`);
}
}
const employee1 = new Employee("Bob", 30, "Manager");
employee1.greet(); // 输出: Hello, my name is Bob and I'm 30 years old. I work as a Manager.
在上面的例子中,Employee
类继承了Person
类,并添加了一个新的属性position
和一个重写的greet
方法。在Employee
类的构造函数中,我们使用super
关键字调用父类的构造函数来初始化继承的属性。在greet
方法中,我们也使用super
来调用父类的greet
方法,并在其后添加额外的输出。
11.3 类的访问修饰符
TypeScript提供了访问修饰符来控制类成员(属性和方法)的可见性。这些修饰符包括public
、private
和protected
。这些修饰符不仅有助于代码封装和组织,还提供了更好的代码可读性和可维护性。
public
:成员可以在任何地方被访问。private
:成员只能在类的内部被访问。protected
:成员只能在类的内部以及派生类中被访问。
在TypeScript中,类成员的可见性是由其访问修饰符决定的。这使得开发者能够更精细地控制哪些部分应该对外部可见,哪些部分应该保持隐藏。这有助于创建更安全、更可靠的代码库,因为隐藏内部实现细节可以减少意外修改和错误使用的风险。
下面是一个使用访问修饰符的示例:
class Animal {
public name: string;
protected health: number;
private isAlive: boolean = true;
constructor(name: string) {
this.name = name;
this.health = 100;
}
public eat(): void {
this.health += 10;
console.log(`${this.name} eats and feels better.`);
}
protected checkHealth(): void {
if (this.health < 50) {
console.log(`${this.name} is not feeling well.`);
}
}
// isAlive 只能在 Animal 类的内部访问
private checkIfAlive(): void {
if (this.isAlive) {
console.log(`${this.name} is alive.`);
}
}
}
class Dog extends Animal {
constructor(name: string) {
super(name);
}
wagTail(): void {
console.log(`${this.name} wags its tail.`);
this.checkHealth(); // 可以访问 protected 成员
// this.checkIfAlive(); // 错误:'checkIfAlive' 是私有的,只能在类 'Animal' 中访问
}
}
// 使用Animal类
const myDog = new Dog('Buddy');
myDog.eat(); // 正确:'eat' 是公共的
console.log(myDog.name); // 正确:'name' 是公共的
// console.log(myDog.health); // 错误:'health' 是受保护的,不能在类外部访问
// console.log(myDog.isAlive); // 错误:'isAlive' 是私有的,不能在类外部访问
// 使用Dog类
myDog.wagTail(); // 正确
在上面的代码中,Animal
类有一个公共属性 name
,一个受保护属性 health
和一个私有属性 isAlive
。eat
方法是公共的,因此可以在类的外部调用。checkHealth
方法是受保护的,这意味着它只能在 Animal
类内部和任何派生类中被访问。checkIfAlive
方法是私有的,因此只能在 Animal
类的内部被访问。
Dog
类继承自 Animal
类,并覆盖了 wagTail
方法。在 wagTail
方法中,我们可以访问 checkHealth
方法,因为它是受保护的。但是,尝试访问 checkIfAlive
方法会导致编译错误,因为它是私有的。
在类的外部,我们创建了一个 Dog
类的实例 myDog
,并调用了它的 eat
方法(因为 eat
是公共的)。我们可以访问 myDog
的 name
属性,因为它是公共的。然而,尝试访问 health
或 isAlive
属性会导致编译错误,因为这些属性是受保护和私有的。
通过适当地使用访问修饰符,我们可以确保类的内部实现细节得到保护,同时提供清晰的接口供外部使用。这有助于创建更加健壮和可维护的 TypeScript 代码库。
12. 函数
函数(Function)是执行特定任务的代码块。在TypeScript中,函数可以有参数,并返回值。TypeScript提供了强大的类型系统来检查函数的参数和返回值类型,从而确保函数在调用时具有正确的输入和输出。
12.1 函数定义
在TypeScript中,函数可以使用多种方式进行定义,包括传统的函数声明、函数表达式和箭头函数。每种方式都有其特定的使用场景和语法规则。
函数声明:
function greet(name: string): void {
console.log(`Hello, ${name}!`);
}
函数表达式:
const greet = function(name: string): void {
console.log(`Hello, ${name}!`);
};
箭头函数:
const greet = (name: string): void => {
console.log(`Hello, ${name}!`);
};
12.2 函数参数
TypeScript允许为函数参数指定类型,从而确保调用函数时提供了正确类型的参数。
function add(a: number, b: number): number {
return a + b;
}
// 正确调用
const sum = add(5, 3); // 返回 8
// 错误调用,TypeScript 会在编译时报错
const wrongSum = add("five", 3); // 类型错误
12.3 函数返回值
TypeScript同样支持对函数返回值进行类型注解,确保函数返回期望的结果类型。
function getLength(str: string): number {
return str.length;
}
const length = getLength("Hello"); // 返回 5
12.4 可选参数与默认参数
TypeScript中的函数支持可选参数和默认参数,这使得函数更加灵活和易用。
function greet(name?: string, greeting: string = "Hello"): void {
if (name) {
console.log(`${greeting}, ${name}!`);
} else {
console.log(greeting);
}
}
greet(); // 输出 "Hello"
greet("Alice"); // 输出 "Hello, Alice!"
12.5 函数重载
函数重载允许一个函数名有多种类型的定义,TypeScript 编译器会根据调用时的参数类型解析应该使用哪个函数签名。
function reverse(x: number): number;
function reverse(x: string): string;
function reverse(x: number | string): number | string {
if (typeof x === 'number') {
return Number(x.toString().split('').reverse().join(''));
} else if (typeof x === 'string') {
return x.split('').reverse().join('');
}
}
const reversedNumber = reverse(12345); // 返回 54321
const reversedString = reverse('hello'); // 返回 'olleh'
在上面的例子中,reverse
函数有两个函数重载声明,分别接受一个数字和一个字符串。然后,它有一个具体的实现,根据传入的参数类型执行不同的操作。
通过使用TypeScript的类型系统,我们可以编写更加健壮和易于维护的代码。函数作为代码中的基本构建块,通过类型注解和类型检查,能够确保我们的程序更加安全和可靠。
13. 泛型
泛型(Generics)是TypeScript中的一个重要特性,它允许在定义函数、接口和类时,不预先指定具体的类型,而在使用的时候再指定类型。泛型可以提高代码的重用性和灵活性,减少不必要的类型转换和代码重复。
13.1 泛型函数
泛型函数是指在定义函数时,不指定具体的参数类型或返回值类型,而是在使用函数时,通过尖括号< >
来指定类型。泛型函数允许我们编写更加通用的代码,避免类型重复定义。
function identity<T>(arg: T): T {
return arg;
}
// 使用泛型函数
let output = identity<string>("myString"); // 类型参数是 string 的类型
let numOutput = identity<number>(123); // 类型参数是 number 的类型
13.2 泛型接口
泛型接口是指在定义接口时,引入一个或多个类型参数。这样,实现接口的类或对象就可以指定具体的类型。泛型接口常用于定义一组具有相似结构的不同类型数据。
interface GenericIdentityFn<T> {
(arg: T): T;
}
function identity<T>(arg: T): T {
return arg;
}
let myIdentity: GenericIdentityFn<number> = identity;
13.3 泛型类
泛型类是指在定义类时,引入类型参数。这样,类的实例可以指定具体的类型。泛型类常用于创建具有不同类型属性的对象。
class GenericNumber<T> {
zeroValue: T;
add: (x: T, y: T) => T;
}
let myGenericNumber = new GenericNumber<number>();
myGenericNumber.zeroValue = 0;
myGenericNumber.add = function(x, y) { return x + y; };
console.log(myGenericNumber.add(10, 20)); // 输出 30
13.4 泛型约束
泛型约束是在使用泛型时,对类型参数进行一定的限制,保证类型参数具备某些特定的属性和方法。这可以通过接口来实现,定义一个接口,泛型类型参数需要符合这个接口,即具备某些特定的属性和方法。
interface Lengthwise {
length: number;
}
function loggingIdentity<T extends Lengthwise>(arg: T): T {
console.log(arg.length); // 现在我们可以访问 arg.length 属性了
return arg;
}
loggingIdentity({length: 10, value: 3}); // 输出 10
13.5 默认泛型类型
在TypeScript中,可以为泛型指定默认类型。当使用泛型时,如果没有显式地指定类型参数,编译器会使用默认类型。
function createArray<T = number>(length: number, value: T): T[] {
let result: T[] = [];
for (let i = 0; i < length; i++) {
result[i] = value;
}
return result;
}
let myDefaultArray = createArray(3); // 使用默认类型 number
let myNumberArray = createArray<number>(5, 100); // 使用显式的 number 类型
let myStringArray = createArray<string>(4, "hello"); // 使用显式的 string 类型
通过以上的介绍,我们了解了TypeScript中泛型的基本概念和使用方法。泛型在编写可重用、类型安全的代码时非常有用,它提高了代码的灵活性和可读性。通过合理地使用泛型,我们可以编写出更加健壮和易于维护的TypeScript程序。
14. 枚举
枚举(Enum)是TypeScript中定义数值集合的一种方式。枚举允许为一组数值定义有意义的名字,提高代码的可读性和可维护性。枚举成员具有明确的数值,可以是数值字面量或计算得出的结果。
14.1 枚举的基本定义
在TypeScript中,我们可以使用enum
关键字来定义枚举类型。枚举类型是一组具名的数值常量集合。例如:
enum Color {
Red,
Green,
Blue
}
let myColor: Color = Color.Red;
在上面的例子中,Color
是一个枚举类型,它有三个成员:Red
、Green
和Blue
。默认情况下,枚举成员会从0开始自增。所以Color.Red
的值为0,Color.Green
的值为1,Color.Blue
的值为2。
14.2 手动赋值
我们也可以为枚举成员手动指定值:
enum Color {
Red = 1,
Green,
Blue
}
console.log(Color.Red); // 输出 1
console.log(Color.Green); // 输出 2
console.log(Color.Blue); // 输出 3
在这个例子中,Red
被显式地赋值为1,之后的成员会在此基础上自增。
14.3 枚举成员的字符串形式
除了数值形式,每个枚举成员还有一个字符串形式,即其名称。可以使用[枚举成员名]
的方式获取:
enum Color {
Red,
Green,
Blue
}
console.log(Color[Color.Red]); // 输出 "Red"
14.4 常量枚举与计算属性
枚举成员可以是常量或计算得出的。常量枚举成员在编译阶段会被替换为其值,不占用额外的存储空间。计算属性则是基于其他枚举成员计算得出的值:
enum Constants {
MAX_VALUE = Math.max(1, 2, 3, 4, 5), // 常量枚举成员
MULTIPLY = 2 * Constants.MAX_VALUE // 计算属性
}
// 常量枚举
const enum Directions {
Up,
Down,
Left,
Right
}
let directions = [Directions.Up, Directions.Down, Directions.Left, Directions.Right];
// 常量枚举在编译后,会被替换为具体的数值
14.5 枚举的反向映射
TypeScript 3.4 引入了枚举的反向映射功能。这意味着除了可以从枚举值映射到枚举名称,还可以从枚举名称映射到枚举值:
enum Color {
Red,
Green,
Blue
}
let colorName = Color[2]; // "Blue"
let colorValue = Color["Green"]; // 1
14.6 枚举的用途
枚举在编程中非常有用,尤其是在需要定义一组固定值集合的场合,如状态码、错误码、配置选项等。它们不仅提高了代码的可读性,还使得代码更易于维护和扩展。通过使用枚举,我们可以避免使用魔法数字或硬编码的字符串,从而提高代码质量。
15. 类型推论
类型推论(Type Inference)是TypeScript的一个强大特性,它允许编译器根据变量的使用上下文自动推断其类型。这意味着在编写代码时,有时无需显式地指定变量的类型,编译器会自动为我们推断出最合适的类型。这种特性极大地提高了代码的可读性和编写效率,同时减少了因类型错误导致的运行时问题。
15.1 类型推论的基本规则
TypeScript 的类型推论遵循一些基本规则。首先,它会尝试从变量的初始化表达式中推断类型。例如,如果你将一个数字赋值给一个变量,那么 TypeScript 就会将该变量的类型推断为 number
。同样地,如果初始化表达式是一个对象字面量,编译器会根据对象的属性和值推断出一个合适的对象类型。
15.2 函数中的类型推论
在函数中,类型推论同样发挥着重要作用。当函数的参数没有显式指定类型时,TypeScript 会根据传递给函数的实际参数的类型来推断参数的类型。同样,如果函数的返回值没有显式指定类型,编译器会根据返回语句中的表达式来推断返回值的类型。
15.3 类型推论与接口和类型的兼容性
类型推论不仅限于简单的变量和函数参数,它还与 TypeScript 中的接口和类型定义紧密相关。编译器会尝试将推断出的类型与接口或类型定义进行匹配,以确保代码的类型安全性。如果推断出的类型与接口或类型定义不兼容,编译器将会报错。
15.4 类型推论的限制与注意事项
虽然类型推论功能强大,但它也有一些限制和注意事项。首先,类型推论并非总是能够准确推断出变量的类型。在某些复杂的情况下,可能需要显式指定变量的类型以避免潜在的类型错误。此外,如果代码中的某些部分没有明确的类型信息,编译器可能无法进行有效的类型检查,这可能导致一些难以察觉的类型错误。
15.5 最佳实践与建议
为了充分利用类型推论的优势并避免潜在的问题,以下是一些最佳实践和建议:
- 尽量使用类型注解:尽管类型推论可以自动推断类型,但在某些情况下,显式地指定类型注解可以提高代码的可读性和可维护性。
- 注意初始化时机:类型推论通常基于变量的初始化表达式。因此,确保在声明变量时立即进行初始化,以便编译器能够准确推断其类型。
- 理解类型推论规则:了解 TypeScript 的类型推论规则有助于预测编译器如何推断类型,并避免潜在的错误。
- 结合使用接口和类型定义:类型推论可以与接口和类型定义一起使用,以提供更精确和灵活的类型检查。
通过遵循这些最佳实践和建议,你可以更有效地利用 TypeScript 的类型推论功能,编写出更加健壮和可维护的代码。
16. 类型兼容性
类型兼容性(Type Compatibility)是TypeScript类型系统中的一个核心概念。它决定了哪些类型的值可以赋值给其他类型的变量,或者在函数调用中使用。TypeScript使用结构子类型化(Structural Subtyping)来判断类型兼容性,即如果源类型的所有成员都在目标类型中存在,则源类型被认为是目标类型的子类型,具有类型兼容性。
16.1 结构与名义兼容性
与一些其他静态类型语言(如Java或C#)不同,TypeScript不使用名义子类型化(Nominal Subtyping),而是依赖于结构子类型化。这意味着两个类型是否兼容,取决于它们结构的相似性,而不是它们是否有一个共同的父类型。这种方法的优点在于其灵活性,可以方便地处理类似于鸭子类型(Duck Typing)的情况,即“如果它走起路来像鸭子,叫起来也像鸭子,那么它就是鸭子”。
16.2 接口与类型兼容性
接口(Interface)在TypeScript中扮演了重要的角色,它们不仅定义了对象的形状,还影响了类型兼容性的判断。如果一个对象满足接口的所有成员要求,即使它的类型并不是显式地基于该接口,它也被认为是与该接口兼容的。
16.3 函数类型兼容性
在函数类型中,兼容性判断相对复杂。TypeScript会检查函数的参数列表和返回类型。如果源函数的参数类型可以被目标函数的参数类型接受,且源函数的返回类型是目标函数返回类型的子类型,则源函数类型与目标函数类型兼容。
16.4 泛型与类型兼容性
泛型(Generics)是TypeScript中处理可重用组件的强大工具。在类型兼容性方面,泛型允许我们创建灵活的类型定义,这些定义可以在不同的上下文中以不同的方式使用,同时保持类型安全。
16.5 类与类型兼容性
在TypeScript中,类(Class)也可以参与类型兼容性的判断。类的实例类型通常与其构造函数类型兼容,同时类的静态成员类型也影响其与其他类型的兼容性。此外,类的继承关系也会影响类型兼容性的判断。
17. 高级类型
TypeScript 提供了许多高级类型,用于更精确地描述数据的形状。这些类型包括但不限于交叉类型(Intersection Types)、联合类型(Union Types)、类型别名(Type Aliases)、索引签名(Index Signatures)、映射类型(Mapped Types)以及条件类型(Conditional Types)。通过组合这些高级类型,可以创建出复杂而强大的类型系统,以满足复杂的编程需求。
17.1 交叉类型(Intersection Types)
交叉类型允许我们将多个类型合并为一个类型,表示一个值同时具有这些类型的所有特性。这在处理复合对象时特别有用,其中对象可能具有来自不同来源的多个属性集。
例如,我们可以定义一个既有 name
属性又有 age
属性的类型:
type Person = { name: string } & { age: number };
const john: Person = {
name: 'John Doe',
age: 30
};
17.2 联合类型(Union Types)
联合类型允许一个变量拥有多种类型之一。它表示一个值可以是几种类型中的任意一种。联合类型非常有用,特别是在处理不同种类的数据或不确定的数据类型时。
例如,一个函数可能返回字符串或数字:
function getRandomValue(): string | number {
// ... some logic to return a string or a number
}
const value = getRandomValue();
value; // Type is string | number
17.3 类型别名(Type Aliases)
类型别名是给类型起一个新名字的方式。它们特别有用,当你有一个复杂的类型定义,并希望简化其在代码中的引用时。
type StringOrNumber = string | number;
function checkValue(value: StringOrNumber) {
// ...
}
17.4 索引签名(Index Signatures)
索引签名用于描述对象那些事先不知道的属性名,但知道属性值类型的场景。它们特别适用于描述字典或具有动态键的对象。
type Dictionary = {
[key: string]: number;
};
const dict: Dictionary = {
a: 1,
b: 2,
c: 3
};
17.5 映射类型(Mapped Types)
映射类型允许我们根据另一个类型来创建新的类型。它们对于操作对象的类型特别有用,例如,将一个对象的所有属性都变为可选,或者添加新的属性。
type Keys = 'a' | 'b' | 'c';
type OptionalObject = { [K in Keys]?: string };
const obj: OptionalObject = {
a: 'valueA',
// b and c are optional
};
17.6 条件类型(Conditional Types)
条件类型允许我们根据条件来判断类型。这在处理复杂的类型逻辑时特别有用,可以根据某些条件来返回不同的类型。
type IsString<T> = T extends string ? true : false;
type Result1 = IsString<'hello'>; // Type is true
type Result2 = IsString<number>; // Type is false
TypeScript 的高级类型特性提供了强大的类型系统和灵活的类型操作方式,使得开发者能够更精确地描述和验证代码的结构和行为,从而增强代码的可读性、可维护性和健壮性。通过深入学习并恰当使用这些特性,开发者可以编写出更加安全、可靠的 TypeScript 代码。
18. Symbols
Symbol 是 ES6 引入的一种新的原始数据类型,表示一个唯一的、不可变的值。在 TypeScript 中,可以使用 Symbol 作为对象的键名,实现属性的唯一性。此外,Symbol 还支持静态方法,如 Symbol.for()
和 Symbol.keyFor()
,用于创建和检索已注册的 symbols。
18.1 Symbol 的基本特性
Symbol 类型的值具有唯一性,这是它最显著的特点。每一个通过 Symbol()
函数创建的 symbol 都是独一无二的,即使使用相同的参数调用 Symbol()
,也会得到不同的 symbol 值。这种特性使得 Symbol 在需要唯一标识符的场景中非常有用,比如定义对象的私有属性或方法。
18.2 使用 Symbol 作为对象键名
在 TypeScript 中,我们可以使用 Symbol 作为对象的键名,从而确保属性的唯一性。由于 Symbol 类型的值是唯一的,因此使用 Symbol 作为键名可以避免属性名的冲突。这在开发大型应用或库时尤其有用,可以确保不同部分的代码不会意外地覆盖彼此的属性。
const uniqueKey = Symbol('unique');
const obj = {
[uniqueKey]: 'Hello, world!',
name: 'John Doe'
};
console.log(obj[uniqueKey]); // 输出 'Hello, world!'
console.log(obj.name); // 输出 'John Doe'
18.3 Symbol 的静态方法
18.3.1 Symbol.for()
Symbol.for()
方法接受一个字符串作为参数,并返回一个以该字符串为名称的 Symbol 值。如果全局环境中已经存在具有相同名称的 Symbol,则 Symbol.for()
会返回这个已经存在的 Symbol,否则它会创建一个新的 Symbol 并将其注册到全局环境中。
const sym1 = Symbol.for('mySymbol');
const sym2 = Symbol.for('mySymbol');
console.log(sym1 === sym2); // 输出 true
18.3.2 Symbol.keyFor()
Symbol.keyFor()
方法接受一个 Symbol 值作为参数,并返回该 Symbol 在全局环境中注册时的名称。如果传入的 Symbol 没有在全局环境中注册,则返回 undefined
。
const sym = Symbol.for('mySymbol');
const name = Symbol.keyFor(sym);
console.log(name); // 输出 'mySymbol'
18.4 Symbol 在 TypeScript 中的应用场景
由于 Symbol 的唯一性特性,它在 TypeScript 中有着广泛的应用场景。以下是一些常见的使用场景:
- 定义私有属性或方法:使用 Symbol 作为对象的键名,可以确保属性或方法的私有性,避免外部代码直接访问或修改。
- 实现枚举或常量:由于 Symbol 的值是唯一的,因此可以使用它来替代传统的字符串枚举或常量,提高代码的安全性和可维护性。
- 防止属性名冲突:在开发大型应用或库时,使用 Symbol 作为对象的键名可以避免属性名的冲突,确保代码的健壮性。
Symbol 是 ES6 引入的一种强大的数据类型,它以其独特的唯一性特性在 TypeScript 中发挥着重要作用。通过使用 Symbol 作为对象的键名、实现私有属性或方法以及防止属性名冲突等方式,我们可以提高代码的安全性、可维护性和健壮性。同时,Symbol 的静态方法如 Symbol.for()
和 Symbol.keyFor()
也为我们提供了更灵活的方式来创建和检索已注册的 symbols。
19. 迭代器和生成器
迭代器是一种设计模式,它允许开发者顺序地访问一个聚合对象中的各个元素,而又不需要暴露该对象的内部表示。在 TypeScript 中,迭代器是通过实现 Iterable
和 Iterator
接口来定义的。生成器是特殊的迭代器,它允许你在函数执行过程中保存上下文,并在需要时恢复执行。生成器通过 function*
语法定义,并使用 yield
关键字来返回值。
19.1 迭代器基础
在 TypeScript 中,迭代器是遵循 Iterator
接口的对象,该接口定义了 next()
方法,它返回一个包含 value
和 done
属性的对象。value
属性表示当前迭代的元素值,而 done
属性是一个布尔值,表示迭代是否完成。
要使一个对象可迭代,它必须实现 Iterable
接口,该接口定义了一个 [Symbol.iterator]()
方法,该方法返回一个迭代器对象。
19.2 使用迭代器
在 TypeScript 中,你可以使用 for...of
循环来遍历可迭代对象。这是因为 for...of
循环内部使用了迭代器来访问集合中的元素。
例如,数组和字符串都是内置的可迭代对象,因此可以直接使用 for...of
循环进行遍历。
19.3 生成器基础
生成器函数使用 function*
语法定义,它允许你定义一个可以多次进入和退出的函数。生成器函数在调用时返回一个生成器对象,这个对象实现了迭代器接口,因此可以使用 for...of
循环进行遍历。
在生成器函数内部,你可以使用 yield
关键字来返回一个值,并暂停函数的执行。当生成器的 next()
方法被调用时,函数将恢复执行,直到遇到下一个 yield
语句或函数结束。
19.4 生成器的应用
生成器在多种场景下都非常有用,尤其是当你需要处理大量数据或执行复杂的异步操作时。由于生成器可以保存函数的执行上下文,它们非常适合用于实现懒加载或无限滚动等功能。
此外,生成器还可以与异步函数结合使用,以简化异步代码的编写。通过使用 async
和 await
关键字,你可以以同步的方式编写异步代码,同时保持生成器的优势。
迭代器和生成器是 TypeScript 中强大的工具,它们允许你以灵活和高效的方式处理集合和函数执行。通过理解这些概念并掌握它们的使用方法,你可以编写出更加简洁、可维护的代码,并处理复杂的编程任务。无论是遍历集合、实现懒加载还是处理异步操作,迭代器和生成器都能提供强大的支持。
20. 模块
模块是 TypeScript 中组织代码的基本单位。通过将代码分割成不同的模块,可以实现代码的复用和隔离。TypeScript 支持 ES6 模块语法,允许你使用 import
和 export
关键字来导入和导出模块成员。此外,TypeScript 还支持 CommonJS 和 AMD 等其他模块加载方案。
20.1 模块基础
在 TypeScript 中,模块是一个包含变量、函数、类或接口等成员的独立文件。每个模块都有自己的作用域,可以避免命名冲突。通过导出(export)模块成员,其他模块可以导入(import)并使用这些成员。
例如,你可以创建一个名为 mathOperations.ts
的模块,并在其中定义一些数学运算函数:
// mathOperations.ts
export function add(a: number, b: number): number {
return a + b;
}
export function multiply(a: number, b: number): number {
return a * b;
}
然后,在另一个文件中,你可以导入并使用这些函数:
// main.ts
import { add, multiply } from './mathOperations';
const sum = add(2, 3);
const product = multiply(2, 3);
console.log(`Sum: ${sum}, Product: ${product}`);
20.2 模块解析
在 TypeScript 中,模块的解析过程依赖于你使用的模块加载器或构建工具。对于 ES6 模块,TypeScript 编译器会生成相应的 ES6 模块代码,然后由运行时环境(如浏览器或 Node.js)负责解析和加载模块。
对于 CommonJS 或 AMD 等其他模块加载方案,TypeScript 提供了相应的配置选项来生成兼容的代码。你可以通过修改 tsconfig.json
文件中的 module
选项来选择不同的模块系统。
20.3 默认导出和命名导出
TypeScript 支持两种类型的导出:默认导出(Default Exports)和命名导出(Named Exports)。默认导出允许你导出一个模块的主要成员,而命名导出则允许你导出多个成员。
默认导出使用 export default
关键字,并且一个模块只能有一个默认导出。命名导出使用 export
关键字,并且一个模块可以有多个命名导出。
在导入时,默认导出需要使用花括号({})进行解构赋值,而命名导出则可以直接使用导入的变量名。
20.4 模块与命名空间
在 TypeScript 的早期版本中,命名空间(Namespaces)是组织代码的一种方式。然而,随着 ES6 模块的普及,命名空间的使用已经逐渐减少。现在,推荐使用模块来组织代码,因为它们与 ES6 标准更加兼容,并且在大型项目中更易于管理和维护。
尽管如此,TypeScript 仍然支持命名空间,并且允许你在同一个文件中同时使用模块和命名空间。但是,为了避免混淆和潜在的错误,建议在一个文件中只使用一种组织代码的方式。
模块是 TypeScript 中组织代码的关键概念之一。通过使用模块,你可以将代码分割成独立的、可复用的单元,从而提高代码的可维护性和可扩展性。TypeScript 支持多种模块加载方案,并且与 ES6 模块语法兼容,使得与其他 JavaScript 工具和技术无缝集成成为可能。通过掌握模块的基础知识和用法,你将能够更有效地在 TypeScript 中组织和管理代码。
21. 命名空间
命名空间是 TypeScript 中用于组织代码的一种机制,它允许你将相关的代码和类型组织在同一个命名空间下,以避免命名冲突。命名空间通过 namespace
关键字定义,其成员可以通过点语法进行访问。虽然 ES6 模块已经成为 JavaScript 和 TypeScript 中组织代码的主流方式,但命名空间在某些场景下仍然具有一定的用途。
21.1 命名空间的基本使用
在 TypeScript 中,你可以使用 namespace
关键字来定义一个命名空间。命名空间内可以包含变量、函数、类、接口、枚举等类型的成员。这些成员在命名空间外部是不可见的,除非你通过命名空间名来访问它们。
namespace MyNamespace {
export const myVariable = "Hello from namespace";
export function greet() {
console.log("Greeting from MyNamespace");
}
}
// 使用命名空间中的成员
console.log(MyNamespace.myVariable); // 输出 "Hello from namespace"
MyNamespace.greet(); // 输出 "Greeting from MyNamespace"
21.2 命名空间与模块的区别
尽管命名空间提供了一种组织代码的方式,但 ES6 模块在现代 JavaScript 和 TypeScript 开发中更为流行。模块通过 import
和 export
语句来导入和导出代码,它们提供了更好的封装性和静态分析的可能性。命名空间是 TypeScript 特有的概念,而模块是 ECMAScript 标准的一部分。
21.3 命名空间的合并
TypeScript 允许你将多个命名空间的定义合并成一个。这在处理大型代码库或第三方库时特别有用,你可以将不同部分的代码组织到同一个命名空间中。
namespace MyNamespace {
export const featureA = "Feature A";
}
namespace MyNamespace {
export const featureB = "Feature B";
}
// 访问合并后的命名空间成员
console.log(MyNamespace.featureA); // 输出 "Feature A"
console.log(MyNamespace.featureB); // 输出 "Feature B"
21.4 命名空间与全局变量
尽管使用命名空间可以减少全局污染,但过度依赖命名空间也可能导致代码结构变得复杂。在 TypeScript 中,如果你希望某个变量或类型在整个项目中都是可见的,而不仅仅是在一个文件或命名空间中,你可能需要使用全局变量或类型声明。
21.5 命名空间与类型声明文件
在 TypeScript 中,类型声明文件(.d.ts
文件)用于为 JavaScript 库提供类型信息。在这些文件中,命名空间经常被用作组织类型定义的一种方式,特别是当这些库没有使用 ES6 模块系统时。
虽然 TypeScript 提供了命名空间作为组织代码的一种方式,但在许多现代项目中,开发者更倾向于使用 ES6 模块来管理代码依赖和封装。然而,在特定的场景下,如处理旧代码库或第三方库时,命名空间仍然是一个有用的工具。
22. 命名空间和模块
命名空间和模块在 TypeScript 中各自扮演着不同的角色,但它们在某些方面也存在交集。命名空间主要用于组织类型和非模块化的代码,而模块则主要用于组织可复用的代码片段。在 TypeScript 中,你可以在一个文件中同时使用命名空间和模块,但需要注意它们之间的作用域和解析规则。
尽管命名空间在某些场景下仍然有用,但随着 ES6 模块的普及和 TypeScript 对模块系统的不断完善,越来越多的开发者倾向于使用模块来组织代码。模块系统提供了更好的封装性、可重用性和可维护性,使得代码更加清晰、易于理解和协作。
22.1 命名空间的基本用法
命名空间在 TypeScript 中是一种将相关的代码组织在一起的方式,它可以帮助我们避免命名冲突,并使得代码结构更加清晰。命名空间通过 namespace
关键字来定义,可以包含类型声明、变量、函数、类、接口等。
namespace MyNamespace {
export interface MyInterface {
name: string;
}
export class MyClass implements MyInterface {
name: string;
constructor(name: string) {
this.name = name;
}
}
}
在上面的例子中,我们定义了一个名为 MyNamespace
的命名空间,它导出了一个接口 MyInterface
和一个类 MyClass
。通过 export
关键字,我们可以使得这些成员在命名空间外部也可访问。
22.2 模块的基本用法
模块是 TypeScript 中组织可复用代码片段的主要方式。与命名空间不同,模块具有更好的封装性和可重用性。模块通过 import
和 export
关键字来定义和使用。
// moduleA.ts
export class ModuleA {
sayHello() {
console.log("Hello from ModuleA!");
}
}
// moduleB.ts
import { ModuleA } from './moduleA';
class ModuleB {
private moduleA: ModuleA;
constructor() {
this.moduleA = new ModuleA();
}
greet() {
this.moduleA.sayHello();
}
}
在上面的例子中,我们定义了两个模块文件 moduleA.ts
和 moduleB.ts
。moduleA.ts
导出了一个类 ModuleA
,而 moduleB.ts
则通过 import
关键字引入了 ModuleA
,并在其内部使用。
22.3 命名空间和模块的交互
虽然命名空间和模块在 TypeScript 中有不同的用途,但它们可以在同一个文件中存在,并且可以相互引用。然而,这种交互方式并不推荐,因为它可能会导致代码结构变得复杂和难以维护。在大多数情况下,最好选择使用模块来组织代码。
22.4 模块的优势和最佳实践
模块系统提供了更好的封装性、可重用性和可维护性。通过模块,我们可以将代码拆分成独立的、可复用的单元,每个模块都具有明确定义的接口和依赖关系。这使得代码更加清晰、易于理解和协作。
在编写 TypeScript 代码时,最佳实践是尽可能使用模块来组织代码。避免过度使用命名空间,除非在特定场景下(如与现有的非模块化代码集成)确实需要。通过使用模块,我们可以更好地利用 TypeScript 的类型检查、代码提示和重构功能,提高开发效率和质量。
命名空间和模块在 TypeScript 中各自扮演着不同的角色,但模块在现代 TypeScript 开发中更为流行和推荐。模块提供了更好的封装性、可重用性和可维护性,使得代码更加清晰和易于协作。在编写 TypeScript 代码时,我们应该尽量遵循最佳实践,使用模块来组织代码,并避免过度依赖命名空间。
23. 模块解析
在TypeScript中,模块解析是确定如何导入和导出模块的过程。TypeScript支持CommonJS、AMD、ES6等多种模块系统,并允许开发者通过配置文件(如tsconfig.json
)来指定模块解析的策略。当使用import
或export
语句时,TypeScript编译器会根据配置的模块系统来解析这些语句,并生成相应的JavaScript代码。
23.1 模块系统概述
TypeScript支持多种模块系统,每种系统都有其特定的语法和用法。CommonJS是最早被广泛使用的模块系统之一,它主要用于Node.js环境。AMD则是为异步加载模块而设计的,特别适用于浏览器环境。而ES6模块是ECMAScript 2015标准中引入的模块系统,它提供了静态的import
和export
语法,以及更好的模块依赖管理。
23.2 tsconfig.json
中的模块配置
tsconfig.json
是TypeScript项目的配置文件,它包含了编译器的各种选项。在tsconfig.json
中,可以通过compilerOptions
字段下的module
属性来指定使用的模块系统。例如,要设置为使用ES6模块,可以这样配置:
{
"compilerOptions": {
"module": "esnext",
// 其他编译选项...
}
}
这里"esnext"
表示使用最新版本的ES模块语法。此外,还可以选择"commonjs"
、"amd"
等其他选项。
23.3 导入模块
在TypeScript中,使用import
语句来导入模块。导入的语法会根据配置的模块系统有所不同。例如,在ES6模块系统中,可以这样导入一个模块:
import * as moduleName from 'module-name';
// 或者
import { specificExport } from 'module-name';
第一个示例导入了整个模块,并将其绑定到moduleName
变量上。第二个示例则只导入了模块中名为specificExport
的导出内容。
23.4 导出模块
导出模块使用export
语句。TypeScript支持默认导出(default exports)和命名导出(named exports)。默认导出用于导出一个模块的单个值或对象,而命名导出则允许导出多个值或对象。
// 默认导出
export default class MyClass {
// ...
}
// 命名导出
export const myVariable = 'Hello, TypeScript!';
export function myFunction() {
// ...
}
23.5 模块解析路径
TypeScript在解析模块时,会按照一定的路径规则来查找模块文件。这些路径规则可以通过tsconfig.json
中的paths
和baseUrl
等选项进行配置。例如,可以配置TypeScript在特定的目录或URL下查找模块,这对于组织大型项目中的模块非常有用。
TypeScript的模块解析是一个灵活且强大的功能,它允许开发者根据项目需求选择适合的模块系统,并通过配置文件来自定义模块解析的行为。通过熟练掌握模块解析的相关知识,可以更加高效地组织和管理TypeScript项目中的代码。
24. 声明合并
TypeScript的声明合并允许开发者将多个声明合并成一个,这在扩展库或框架的类型定义时非常有用。例如,你可以为一个已有的接口添加新的方法或属性,或者为全局对象添加新的类型定义。这种灵活性使得TypeScript能够更好地适应各种复杂的代码库和场景。
24.1 接口合并
接口合并是TypeScript声明合并中最常见的用法之一。当我们在同一个作用域内定义多个具有相同名称的接口时,TypeScript会将它们合并成一个接口。这允许我们逐步构建接口,或者为第三方库扩展接口。
interface User {
name: string;
age: number;
}
interface User {
address: string;
}
const user: User = {
name: 'John Doe',
age: 30,
address: '123 Street'
};
在这个例子中,我们定义了两个名为User
的接口,TypeScript将它们合并成一个包含name
、age
和address
属性的接口。
24.2 命名空间与类的合并
除了接口,命名空间(在TypeScript 2.0+中推荐使用模块)和类也可以进行声明合并。这允许我们为已有的类或命名空间添加新的方法、属性或子模块。
namespace MyNamespace {
export const value = 1;
}
namespace MyNamespace {
export function printValue() {
console.log(MyNamespace.value);
}
}
MyNamespace.printValue(); // 输出: 1
24.3 全局变量和模块合并
全局变量声明和模块导出之间也可以进行合并。这允许我们在全局作用域中定义类型,同时也可以在模块内部对这些类型进行扩展。
declare global {
interface Window {
myCustomFunction?: () => void;
}
}
window.myCustomFunction = () => {
console.log('Custom function called!');
};
在这个例子中,我们声明了一个全局的Window
接口,为其添加了一个可选的myCustomFunction
方法。然后,我们可以在全局window
对象上定义这个方法。
24.4 模块扩展
当我们需要扩展第三方模块的类型定义时,声明合并同样非常有用。例如,我们可以为Node.js的内置模块或流行的npm包添加新的类型定义。
// 假设有一个第三方库,我们想要为其添加类型定义
declare module 'third-party-library' {
export interface Options {
newOption?: string;
}
export function create(options?: Options): void;
}
import { create, Options } from 'third-party-library';
create({ newOption: 'value' });
声明合并是TypeScript中一个强大且灵活的特性,它允许我们逐步构建和扩展类型定义,而不需要修改原始代码。这使得TypeScript能够更好地与现有的JavaScript库和框架集成,同时保持类型安全性。通过利用声明合并,我们可以为复杂的代码库提供清晰的类型定义,从而提高代码的可读性和可维护性。
25. JSX
JSX是React等库用于描述UI的一种语法扩展,它允许在JavaScript代码中编写类似HTML的结构。TypeScript原生支持JSX,并允许开发者在类型系统中定义JSX元素的属性和子元素的类型。这使得在TypeScript中使用JSX编写的代码具有更好的类型安全性和可维护性。
25.1 JSX基础语法
在TypeScript中使用JSX,首先需要配置tsconfig.json文件,确保"jsx": "react"
或相应的值被设置。之后,你可以在.tsx
文件中使用JSX语法。例如:
import React from 'react';
interface MyComponentProps {
name: string;
age?: number;
}
const MyComponent: React.FC<MyComponentProps> = ({ name, age }) => {
return (
<div>
<h1>Hello, {name}!</h1>
{age && <p>Age: {age}</p>}
</div>
);
}
export default MyComponent;
在这个例子中,我们定义了一个React函数组件MyComponent
,它接受一个名为name
的必需属性和一个可选的age
属性。在JSX中,我们使用了这些属性,并且TypeScript能够确保我们正确地传递了这些属性。
25.2 类型检查与属性推断
TypeScript能够推断JSX元素的类型,并对其进行类型检查。如果尝试传递错误的属性或子元素,TypeScript编译器会报错。例如:
// 假设有如下组件定义
interface GreetingProps {
greeting: string;
}
const Greeting: React.FC<GreetingProps> = ({ greeting }) => {
return <h1>{greeting}</h1>;
}
// 正确的使用方式
<Greeting greeting="Hello, World!" />
// 错误的使用方式,TypeScript会报错
<Greeting message="Hello, World!" /> // 属性名错误
<Greeting greeting={123} /> // 属性类型错误
25.3 子元素类型
在TypeScript中,你可以通过React.ReactNode或自定义类型来定义JSX元素的子元素类型。React.ReactNode表示任何有效的React节点,包括文本、元素和组件等。
interface ListItemProps {
children: React.ReactNode;
}
const ListItem: React.FC<ListItemProps> = ({ children }) => {
return <li>{children}</li>;
}
// 使用方式
<ListItem>
<span>This is an item</span>
</ListItem>
25.4 泛型与JSX
TypeScript的泛型可以与JSX结合使用,以增强代码的可重用性和灵活性。例如,你可以创建一个接受任意类型属性的泛型组件:
interface GenericComponentProps<T> {
data: T;
}
const GenericComponent: React.FC<GenericComponentProps<any>> = ({ data }) => {
// 根据data的类型,渲染不同的内容
return <div>{JSON.stringify(data)}</div>;
}
// 使用方式
<GenericComponent data={{ name: "John", age: 30 }} />
<GenericComponent data={[1, 2, 3, 4, 5]} />
TypeScript通过提供对JSX的原生支持,使得在React等库中编写类型安全的UI代码成为可能。通过定义接口和类型,我们可以确保组件的属性、子元素和事件处理等都具有正确的类型,从而提高代码的可维护性和减少运行时错误。同时,TypeScript的泛型特性也使得我们可以创建更加灵活和可重用的组件。TypeScript与JSX的结合是构建健壮且易于维护的React应用的关键之一。
26. 装饰器
装饰器是ES7提出的一项实验性特性,TypeScript提供了对装饰器的支持。装饰器是一种特殊类型的声明,它可以被附加到类声明、方法、属性或参数上。装饰器使用@
表达式,并且紧跟在被装饰的声明之前。它们可以用于修改类的行为或属性,实现一些诸如依赖注入、日志记录等高级功能。
26.1 装饰器基础
装饰器的基本语法非常简单,通过在类或方法声明前加上@
符号和装饰器名称来应用装饰器。装饰器函数会接收被装饰的声明作为参数,并可以对其进行修改或增强。
例如,一个简单的类装饰器可能如下所示:
function sealed(constructor: Function) {
Object.seal(constructor);
Object.seal(constructor.prototype);
}
@sealed
class Greeter {
greeting: string;
constructor(message: string) {
this.greeting = message;
}
greet() {
return "Hello, " + this.greeting;
}
}
在这个例子中,@sealed
装饰器用于将Greeter
类和它的原型对象设置为不可扩展的,这有助于确保类的结构在运行时不会被意外修改。
26.2 装饰器工厂
装饰器也可以是一个工厂函数,它返回一个装饰器函数。工厂函数允许装饰器接收参数,并基于这些参数定制装饰器的行为。
function loggable(name: string) {
return function (target: Function) {
console.log(`Logging for ${name} is enabled.`);
};
}
@loggable('Greeter')
class Greeter {
// ...
}
在这个例子中,@loggable('Greeter')
会根据提供的name
参数在控制台打印一条消息。
26.3 类装饰器
类装饰器在类声明之前应用,它们接收类的构造函数作为参数。类装饰器可以用来修改类的结构,添加或修改静态属性,或者执行其他影响整个类的操作。
function classDecorator<T extends { new (...args: any[]): {} }>(constructor: T) {
return class extends constructor {
newProperty = "new property";
hello = "overridden";
}
}
@classDecorator
class MyClass {
property = "property";
hello = "hello";
}
const obj = new MyClass();
console.log(obj.newProperty); // 输出 "new property"
console.log(obj.hello); // 输出 "overridden"
26.4 方法装饰器
方法装饰器在方法声明之前应用,它们接收三个参数:类的原型对象、方法名和描述符对象。方法装饰器可以用来修改方法的实现,或者添加额外的功能。
function methodDecorator(
target: Object,
propertyKey: string | symbol,
descriptor: TypedPropertyDescriptor<any>
) {
const originalMethod = descriptor.value;
descriptor.value = function (...args: any[]) {
console.log(`Calling ${propertyKey.toString()} with`, args);
const result = originalMethod.apply(this, args);
console.log(`Called ${propertyKey.toString()}`);
return result;
};
return descriptor;
}
class MyClass {
@methodDecorator
greet(name: string) {
return `Hello ${name}!`;
}
}
const obj = new MyClass();
console.log(obj.greet('TypeScript')); // 输出调用和结果信息
26.5 属性装饰器
属性装饰器在属性声明之前应用,它们接收两个参数:类的原型对象和属性名。属性装饰器可以用来观察或修改属性的行为。
function propertyDecorator(target: Object, propertyKey: string) {
console.log(`Property ${propertyKey} is decorated.`);
}
class MyClass {
@propertyDecorator
myProperty = 123;
}
在这个例子中,@propertyDecorator
装饰器仅用于在控制台输出一条消息,表明属性已经被装饰。在实际应用中,属性装饰器可以用于诸如属性访问权限控制、数据验证或自动序列化等更复杂的场景。
26.6 类装饰器
类装饰器在类声明之前应用,它们接收一个参数:类的构造函数。类装饰器可以用来修改类的结构或添加额外的功能。
function classDecorator(constructor: Function) {
console.log(`Class ${constructor.name} is decorated.`);
}
@classDecorator
class MyClass {
// 类的方法和属性...
}
在这个例子中,@classDecorator
装饰器仅用于在控制台输出一条消息,表明类已经被装饰。在实际应用中,类装饰器可以用于诸如依赖注入、日志记录或性能监控等场景。
26.7 参数装饰器
参数装饰器在参数声明之前应用,它们接收三个参数:类的原型对象、方法名和参数索引。参数装饰器可以用来观察或修改方法参数的行为。
function parameterDecorator(
target: Object,
propertyKey: string | symbol,
parameterIndex: number
) {
console.log(`Parameter at index ${parameterIndex} of ${propertyKey.toString()} is decorated.`);
}
class MyClass {
greet(@parameterDecorator name: string) {
return `Hello ${name}!`;
}
}
参数装饰器可以用于验证方法参数的输入,或者在某些场景下,自动处理或转换参数值。
装饰器的注意事项
虽然装饰器为TypeScript提供了强大的扩展能力,但在使用它们时需要注意以下几点:
-
实验性特性:装饰器在TypeScript中仍然是实验性特性,这意味着它们可能在未来的版本中发生变化或被移除。因此,在使用装饰器时,建议查阅最新的TypeScript文档以了解最新的进展和最佳实践。
-
兼容性:由于装饰器是TypeScript的特性,而不是JavaScript原生支持的,因此在使用装饰器时需要确保目标环境支持TypeScript或使用了相应的转译工具(如Babel)来将TypeScript代码转换为JavaScript代码。
-
性能考虑:装饰器在运行时会对代码进行额外的处理,这可能会对性能产生一定的影响。因此,在性能敏感的场景中,需要谨慎使用装饰器,并进行必要的性能测试和优化。
装饰器是TypeScript中一个强大的特性,可以用于扩展类、方法、属性和参数的行为。它们为开发者提供了更多的灵活性和控制力,但也需要谨慎使用,并注意其实验性特性和性能影响。通过合理地使用装饰器,我们可以构建出更加健壮、可维护和可扩展的TypeScript应用程序。
27. Mixins
Mixins是一种代码复用模式,允许一个类继承多个类的功能。虽然TypeScript本身并没有直接提供Mixins的语法糖,但可以通过一些技巧(如使用交叉类型或类型别名)来实现类似的功能。Mixins在需要组合多个类的功能时非常有用,可以提高代码的可读性和可维护性。
27.1 Mixins的基本概念
Mixins是一种设计模式,它允许开发者将一个类的功能“混入”到另一个类中,从而实现代码的复用和功能的组合。在TypeScript中,虽然语言本身没有直接支持Mixins的语法,但我们可以通过一些类型系统和类的组合技巧来模拟Mixins的行为。
27.2 使用交叉类型实现Mixins
在TypeScript中,交叉类型(Intersection Types)可以用于模拟Mixins的效果。交叉类型允许我们将多个类型合并成一个类型,从而组合多个类的属性和方法。通过定义一个包含所需功能的接口,并将其与基础类进行交叉类型组合,我们可以实现类似Mixins的功能。
例如,假设我们有两个功能接口:Flyable
和 Swimmable
,分别表示飞行和游泳的能力。我们可以创建一个基础类 Animal
,并通过交叉类型将 Flyable
和 Swimmable
混入 Animal
类中,创建一个具有飞行和游泳能力的动物类。
interface Flyable {
fly(): void;
}
interface Swimmable {
swim(): void;
}
class Animal {
eat(): void {
console.log('The animal eats.');
}
}
type FlyingSwimmingAnimal = Animal & Flyable & Swimmable;
class BirdAndFish implements FlyingSwimmingAnimal {
fly(): void {
console.log('The animal flies.');
}
swim(): void {
console.log('The animal swims.');
}
eat(): void {
console.log('The bird and fish eats.');
}
}
在这个例子中,BirdAndFish
类实现了 FlyingSwimmingAnimal
类型,该类型是通过交叉类型组合了 Animal
、Flyable
和 Swimmable
接口得到的。这样,BirdAndFish
类就具有了飞行、游泳和吃食的能力。
27.3 使用高阶组件实现Mixins
除了使用交叉类型外,我们还可以借助高阶组件(Higher-Order Components, HOCs)的概念来实现Mixins。高阶组件是一个接受组件并返回一个新组件的函数。在TypeScript中,这可以通过泛型函数和类型参数来实现。
高阶组件允许我们将多个功能组件“包裹”在一个组件中,从而实现功能的组合。每个高阶组件都可以添加特定的属性和方法到被包裹的组件上。
27.4 Mixins的优势和局限性
Mixins的优势在于能够灵活地组合多个类的功能,避免了继承层次过深导致的问题,并且提高了代码的可读性和可维护性。通过Mixins,我们可以将功能划分为更小的、可重用的单元,使得代码更加模块化和可测试。
然而,Mixins也存在一些局限性。首先,由于TypeScript本身没有直接支持Mixins的语法,实现起来可能相对复杂,需要借助一些技巧和类型系统。其次,过度使用Mixins可能导致代码难以理解和维护,特别是在处理复杂的类组合和继承关系时。
Mixins是一种强大的代码复用模式,在TypeScript中可以通过交叉类型和高阶组件等方式实现。虽然TypeScript没有直接提供Mixins的语法糖,但通过合理的技巧和类型系统设计,我们可以充分利用Mixins的优势来提高代码的可读性、可维护性和可重用性。然而,在使用Mixins时也需要注意其局限性,避免过度使用导致代码结构混乱。
28. 三斜线指令
三斜线指令是TypeScript特有的一种语法,用于在文件中包含类型定义或其他元数据信息。例如,/// <reference types="node"/>
这样的指令告诉TypeScript编译器包含Node.js的类型定义。这种机制使得开发者能够方便地引用和管理类型定义文件,从而提高代码的类型安全性。
28.1 三斜线指令的基本语法
三斜线指令的基本语法以///
开头,后面紧跟一个空格和尖括号包围的指令内容。这些指令可以是类型引用、库引用或者其他的元数据设置。编译器在解析TypeScript文件时会识别这些指令,并根据指令内容执行相应的操作。
28.2 类型定义文件的引用
最常见的三斜线指令是引用类型定义文件。当使用第三方库或者框架时,这些库或框架可能没有内置的类型定义。此时,开发者可以通过三斜线指令引用外部的类型定义文件,从而在TypeScript中获得类型检查和类型推断的能力。例如,/// <reference types="lodash"/>
将告诉编译器引入lodash库的类型定义。
28.3 库文件的引用
除了类型定义文件,三斜线指令还可以用于引用库文件。这通常用于将全局变量或函数声明添加到TypeScript的编译上下文中。例如,当使用某些全局库时,这些库可能没有使用模块化的方式导出,而是直接定义在全局作用域中。此时,可以使用三斜线指令来声明这些全局变量或函数,以便在TypeScript中能够正确地引用它们。
28.4 其他元数据设置
除了类型定义和库文件的引用,三斜线指令还可以用于其他元数据设置。这些设置可能因TypeScript版本或特定项目的需求而有所不同。例如,某些项目可能使用三斜线指令来配置编译器的某些行为,或者为代码生成器提供额外的信息。这些设置通常具有特定的语法和格式,需要参考TypeScript的官方文档或项目文档来了解更多细节。
28.5 注意事项与最佳实践
使用三斜线指令时需要注意以下几点:
- 明确性:确保指令的内容清晰明确,避免使用模糊或含糊不清的引用。
- 可维护性:将三斜线指令放在文件的顶部或特定的区域,方便其他开发者查看和理解。
- 版本兼容性:不同的TypeScript版本可能对三斜线指令的支持有所不同,确保所使用的指令与项目所依赖的TypeScript版本兼容。
- 替代方案:在某些情况下,可能存在更优雅或更高效的替代方案来实现相同的功能。例如,使用npm包管理类型定义文件可能比手动添加三斜线指令更方便。
通过遵循这些注意事项和最佳实践,开发者可以更有效地利用三斜线指令来提高TypeScript代码的类型安全性和可维护性。
29. JavaScript文件类型检查
TypeScript不仅可以对.ts
文件进行类型检查,还可以通过配置对.js
文件进行类型检查。这允许开发者在逐渐迁移到TypeScript的过程中,仍然能够享受到类型检查带来的好处。通过配置allowJs
和checkJs
选项,TypeScript编译器可以对JavaScript文件进行静态类型分析,并报告潜在的类型错误。这有助于发现JavaScript代码中的潜在问题,提高代码质量。
29.1 配置TypeScript以检查JavaScript文件
首先,你需要在你的tsconfig.json
文件中设置allowJs
和checkJs
选项为true
。这将告诉TypeScript编译器包含JavaScript文件,并对它们进行类型检查。
{
"compilerOptions": {
"target": "es6",
"module": "commonjs",
"allowJs": true, // 允许编译JavaScript文件
"checkJs": true, // 对JavaScript文件进行类型检查
"strict": true, // 开启所有严格类型检查选项
// 其他选项...
},
"include": [
"src/**/*.js", // 指定要包含哪些JavaScript文件
// 其他文件模式...
],
"exclude": [
"node_modules", // 排除不需要类型检查的目录
// 其他目录...
]
}
29.2 JSDoc注释的使用
由于JavaScript本身没有类型系统,TypeScript使用JSDoc注释来推断JavaScript代码的类型。在JavaScript文件中添加JSDoc注释可以帮助TypeScript编译器更好地理解代码的结构和类型。
/**
* @param {string} name - 用户的名字
* @param {number} age - 用户的年龄
* @returns {boolean} - 是否是成年人
*/
function isAdult(name, age) {
return age >= 18;
}
在上面的例子中,通过JSDoc注释,我们告诉TypeScript编译器isAdult
函数接受一个字符串类型的name
和一个数字类型的age
,并返回一个布尔值。
29.3 处理潜在的类型错误
当TypeScript编译器在检查JavaScript文件时遇到类型错误,它会在控制台中输出错误消息。开发者需要查看这些错误消息,并根据需要对代码进行修复。
例如,如果有一个函数期望接收一个字符串,但实际上传递了一个数字,TypeScript会报告一个类型错误。开发者需要修正这个错误,确保传递正确的参数类型。
29.4 逐步迁移到TypeScript
通过允许对JavaScript文件进行类型检查,TypeScript为开发者提供了一个平滑的迁移路径。开发者可以逐步将现有的JavaScript代码库迁移到TypeScript,而不是一次性重写所有代码。这降低了迁移的复杂性和风险,同时让开发者能够逐渐享受到TypeScript带来的类型安全和可维护性。
29.5 注意事项
尽管TypeScript对JavaScript文件的类型检查功能非常有用,但也有一些限制和注意事项。首先,由于JavaScript本身没有类型系统,因此类型推断可能不如纯TypeScript代码准确。此外,某些复杂的JavaScript模式可能难以用JSDoc准确描述,这可能导致类型检查不够精确或产生误报。因此,在使用此功能时,开发者需要谨慎对待类型检查的结果,并结合代码逻辑和测试来确保代码的正确性。
总结
TypeScript作为一个强大的静态类型系统,为JavaScript开发者提供了更好的代码安全性和可维护性。通过掌握上述基础知识点,开发者可以更高效地利用TypeScript来构建高质量的前端应用程序。同时,TypeScript的灵活性和扩展性也使得它能够适应各种复杂的场景和需求。随着前端技术的不断发展,TypeScript将继续发挥其在构建大型、复杂应用中的重要作用。