ARTICLE DETAIL

建站实战干货

来自一线的建站与推广经验沉淀,每一条都经过真实交付验证。

TypeScript基础知识点

2026/8/23 19:02:27 拓冰建站 浏览量
TypeScript基础知识点

TypeScript基础知识点

  • 一、TypeScript概述
    • 1.产生背景
    • 2.主要功能
      • 2.1 TypeScript 与 JavaScript
        • 2.x.x TypeScript 与 JavaScript的关系
        • 2.x.x TypeScript 与 JavaScript的不同
        • 2.x.x TypeScript 与 JavaScript 的兼容性
    • 3.主要特性
      • 3.1 类型批注
        • 3.1.1 TypeScript代码案例:Add函数
      • 3.2 声明文件
        • 3.2.1 TypeScript代码案例:模块
        • 3.2.2 TypeScript 代码案例:类Class
      • 3.3 泛型
    • 4.开发工具
      • 4.1 IDE 和编辑器支持
      • 4.1.1 Visual Studio 2019
      • 4.1.2 Visual Studio Code
      • 4.1.3 Visual Studio 2017
    • 5.TypeScript 开源
    • 6.TypeScript 5.0正式发布!(2023年3月17日)
      • 6.1 TypeScript 5.0的安装
      • 6.2 TypeScript 5.0 的主要更新
        • 6.2.1 全新装饰器
        • 6.2.2 const 类型参数
        • 6.2.3 extends 支持多配置文件
        • 6.2.4 所有枚举都是联合枚举
        • 6.2.5 --moduleResolutionbundler
        • 6.2.6 自定义解析标志
          • 6.2.6.1 allowImportingTsExtensions
          • 6.2.6.2 resolvePackageJsonExports
          • 6.2.6.3 resolvePackageJsonImports
          • 6.2.6.4 allowArbitraryExtensions
        • 6.2.7 --verbatimModuleSyntax
        • 6.2.8 支持 export type *
        • 6.2.9 JSDoc 支持 @satisfies
        • 6.2.10 JSDoc 支持 @overload
        • 6.2.11 编辑器中不区分大小写的导入排序
        • 6.2.12 完善 switch/case
        • 6.2.13 优化速度、内存和包大小
        • 6.2.14 其他重大更改和弃用
    • 7. 5分钟用TypeScript创建一个简单的Web应用
      • 7.1 安装TypeScript
      • 7.2 构建你的第一个TypeScript文件
      • 7.3 编译代码
      • 7.4 类型注解
      • 7.5 接口
      • 7.6 类
      • 7.7 运行TypeScript Web应用
    • 8.TypeScript基础类型
      • 8.1 TypeScript布尔值
      • 8.2 TypeScript数字
      • 8.3 TypeScript字符串
      • 8.5 TypeScript元组 Tuple
      • 8.6 TypeScript枚举
      • 8.7 TypeScript的Any
      • 8.8 TypeScript的Void
      • 8.9 TypeScript的Null和Undefined
      • 8.10 TypeScript的Never
      • 8.11 TypeScript的Object
      • 8.12 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概述

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.x.x TypeScript 与 JavaScript的关系

JavaScript 是世界上最常用的编程语言之一,已成为 Web 官方语言。 开发人员使用它来编写可在任何平台和任何浏览器中运行的跨平台应用程序。

尽管 JavaScript 用于创建跨平台应用,但它并不是为涉及数千甚至数百万行代码的大型应用设计的。 JavaScript 缺少一些更成熟的语言所具备的功能,这些功能可为当今复杂的应用程序提供支持。 用集成开发编辑器 (IDE) 管理 JavaScript 和维护这些大型代码库颇具挑战。

TypeScript 打破了 JavaScript 的局限性,且不会因此影响 JavaScript 的关键价值主张:能够在任何地方使用任何平台、浏览器或主机运行代码。

TypeScript 是由 Microsoft 开发的一种开放源代码语言。 它是 JavaScript 的一个超集,这意味着你可以使用已开发的 JavaScript 技能,以及以前不可用的某些功能。


2.x.x TypeScript 与 JavaScript的不同
对比TypeScriptJavaScript
类型系统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.x.x 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 时,请记住在几乎所有情况下,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 和编辑器支持

微软官方推荐的编辑器有:

TypeScript基础知识点

4.1.1 Visual Studio 2019


TypeScript基础知识点

4.1.2 Visual Studio Code


TypeScript基础知识点

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;}@loggedMethodgreet() {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@loggedMethodgreet() {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);

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 typescriptoutput 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 现在会检测被检查的值何时具有字面量类型。以提供更便利的代码快捷输入:
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.9TypeScript5.0
63.8 MB37.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一起工作。

TypeScript基础知识点


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' 都有 toStringx[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); // OKcreate(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变量声明

10.接口

11.类

12.函数

13.泛型

14.枚举

15.类型推论

16.类型兼容性

17.高级类型

18.Symbols

19.迭代器和生成器

20.模块

21.命名空间

22.命名空间和模块

23.模块解析

24.声明合并

25.JSX

26.装饰器

27.Mixins

28.三斜线指令

29.JavaScript文件类型检查