TypeScript 文档

一  安装

// 在项目中安装
npm install typescript --save-dev
yarn add typescript --dev

// 全局安装
npm install -g typescript

yarn global add typescript

二 基本概念

TypeScript 提供了 JavaScript 的所有功能,以及在这些功能之上的附加层:TypeScript 的类型系统。

1. 类型推理

TypeScript 了解 JavaScript 语言,并且会在许多情况下为您生成类型。例如,在创建变量并将其分配给特定值时,TypeScript 将使用该值作为其类型。

 2. 定义类型

您可以在 JavaScript 中使用多种设计模式。但是,某些设计模式使自动推断类型变得困难(例如,使用动态编程的模式)。为了解决这些情况,TypeScript 支持 JavaScript 语言的扩展,它为您提供了 TypeScript ,告诉您应该什么的地方应该是什么类型。

例如,要创建一个包含 name: string 和 id: number 的推断类型的对象,您可以编写:

您可以使用interface声明显式描述此对象的形状

然后,您可以在变量声明之后使用类似以下的语法来声明 JavaScript 对象符合interface:

如果你提供的对象与你提供的接口不匹配,TypeScript 会警告你 

由于 JavaScript 支持类和面向对象编程,因此 TypeScript 也支持。您可以对类使用接口声明: 

 您可以使用接口来注释参数并将值返回给函数:

JavaScript 中已经有一小组可用的原始类型:boolean、bigint、null、number、string、symbol 和 undefined,您可以在接口中使用它们。TypeScript 扩展了这个列表,例如 any (允许任何类型) unknow (确保使用此类型的人声明该类型是什么) ,never(这种类型不可能发生),void(返回未定义或没有返回值的函数).

您将看到构建类型有两种语法:interface和type。你应该更喜欢interface。当您需要特定功能时使用类型.

3. 构建类型

使用 TypeScript,您可以通过组合简单类型来创建复杂类型。有两种流行的方法可以做到这一点:使用联合和使用泛型。

3.1 联合类型

联合类型的一个流行用例是描述允许值是的字符串或数字文字集:

type WindowStates = "open" | "closed" | "minimized";
type LockStates = "locked" | "unlocked";
type PositiveOddNumbersUnderTen = 1 | 3 | 5 | 7 | 9;

联合也提供了一种处理不同类型的方法。例如,您可能有一个接受数组或字符串的函数:

function getLength(obj: string | string[]) {
  return obj.length;
}

要了解变量的类型,请使用 typeof:

TypePredicate
stringtypeof s === "string"
numbertypeof n === "number"
booleantypeof b === "boolean"
undefinedtypeof undefined === "undefined"
functiontypeof f === "function"
arrayArray.isArray(a)

例如,您可以根据传递的是字符串还是数组,使函数返回不同的值:

 3.2 泛型

泛型为类型提供变量。一个常见的例子是数组。没有泛型的数组可以包含任何东西。带有泛型的数组可以描述数组包含的值

type StringArray = Array<string>;
type NumberArray = Array<number>;
type ObjectWithNameArray = Array<{ name: string }>;

您可以声明自己的使用泛型的类型:

4. 静态类型检测

回想一下我们之前尝试将字符串作为函数调用时得到的 TypeError。大多数人不喜欢在运行他们的代码时出现任何类型的错误——这些被认为是错误!并且当我们编写新代码时,我们会尽量避免引入新的错误。

如果我们只添加一点代码,保存我们的文件,重新运行代码,并立即看到错误,我们可能能够快速隔离问题;但情况并非总是如此。我们可能没有足够彻底地测试该功能,因此我们可能永远不会真正遇到可能抛出的潜在错误!或者,如果我们有幸目睹错误,我们可能最终会进行大规模重构并添加许多我们被迫深入研究的不同代码。

实际上,我们可以有一个工具来帮助我们在代码运行之前找到这些错误。这就是像 TypeScript 这样的静态类型检查器所做的。静态类型系统描述了我们运行程序时我们的值的形状和行为。像 TypeScript 这样的类型检查器使用这些信息并告诉我们什么时候事情可能错了。

 5.非异常故障

到目前为止,我们一直在讨论某些事情,比如运行时错误——JavaScript 运行时告诉我们它认为某些东西是无意义的情况。出现这些情况是因为 ECMAScript 规范明确说明了语言在遇到意外情况时应该如何表现。

例如,规范说尝试调用不可调用的东西应该抛出错误。也许这听起来像是“明显的行为”,但您可以想象访问对象上不存在的属性也应该抛出错误。相反,JavaScript 给了我们不同的行为并返回 undefined 值:

const user = {
  name: "Daniel",
  age: 26,
};
user.location; // returns undefined

最终,静态类型系统必须调用其系统中应该将哪些代码标记为错误的代码,即使它是不会立即抛出错误的“有效”JavaScript。在 TypeScript 中,以下代码会产生关于未定义位置的错误:

虽然有时这意味着在您可以表达的内容上进行权衡,但目的是在我们的程序中捕获合法的错误。而且 TypeScript 捕获了很多合法的错误。

例如:错别字, 

const announcement = "Hello World!";
 
// How quickly can you spot the typos?
announcement.toLocaleLowercase();
announcement.toLocalLowerCase();
 
// We probably meant to write this...
announcement.toLocaleLowerCase();

未调用的函数,

或基本逻辑错误。

 6.类型工具

 当我们在代码中出错时,TypeScript 可以捕获错误。这很好,但 TypeScript 也可以首先防止我们犯这些错误。

类型检查器有信息来检查诸如我们是否正在访问变量和其他属性的正确属性。一旦有了这些信息,它还可以开始建议您可能想要使用的属性。

这意味着也可以利用 TypeScript 来编辑代码,并且核心类型检查器可以在您在编辑器中键入时提供错误消息和代码完成。这是人们在谈论 TypeScript 中的工具时经常提到的部分内容。

 TypeScript 非常重视工具,这超出了您键入时的完成和错误。支持 TypeScript 的编辑器可以提供“快速修复”以自动修复错误、重构以轻松重新组织代码,以及用于跳转到变量定义或查找给定变量的所有引用的有用导航功能。所有这些都建立在类型检查器之上,并且是完全跨平台的,因此您最喜欢的编辑器很可能具有可用的 TypeScript 支持。

7.tsc , TypeScript 编译器

我们一直在谈论类型检查,但我们还没有使用我们的类型检查器。让我们熟悉我们的新朋友 tsc,TypeScript 编译器。首先,我们需要通过 npm 获取它。

npm install -g typescript

这将全局安装 TypeScript Compiler tsc。如果您更喜欢从本地 node_modules 包运行 tsc,则可以使用 npx 或类似工具。

现在让我们新建到一个空文件夹并尝试编写我们的第一个 TypeScript 程序:hello.ts:

// Greets the world.
console.log("Hello world!");

请注意,这里没有多余的装饰;这个“hello world”程序看起来与你在 JavaScript 中为“hello world”程序编写的程序相同。现在让我们通过运行 typescript 包为我们安装的命令 tsc 来检查它。

tsc hello.ts

Tada!

等等,“tada”究竟是什么?我们运行了 tsc 并没有发生任何事情!好吧,没有类型错误,所以我们没有在控制台中得到任何输出,因为没有什么可报告的。

但是再检查一下 - 我们得到了一些文件输出。如果我们查看当前目录,我们会在 hello.ts 旁边看到一个 hello.js 文件。这是 hello.ts 文件在 tsc 编译或转换为纯 JavaScript 文件后的输出。如果我们检查内容,我们将看到 TypeScript 在处理 .ts 文件后吐出的内容:

// Greets the world.
console.log("Hello world!");

在这种情况下,TypeScript 几乎没有要转换的内容,因此它看起来与我们编写的内容相同。编译器会尝试发出清晰可读的代码,看起来像一个人会写的东西。虽然这并不总是那么容易,但 TypeScript 始终如一地缩进,注意我们的代码何时跨越不同的代码行,并尝试保留注释。

如果我们确实引入了类型检查错误呢?让我们重写hello.ts:

// This is an industrial-grade general-purpose greeter function:
function greet(person, date) {
  console.log(`Hello ${person}, today is ${date}!`);
}
 
greet("Brendan");

如果我们再次运行 tsc hello.ts,请注意我们在命令行上得到一个错误!

Expected 2 arguments, but got 1.

TypeScript 告诉我们我们忘记将参数传递给 greet 函数,这是理所当然的。到目前为止,我们只编写了标准的 JavaScript,但是类型检查仍然能够发现我们代码的问题。感谢typescript!

8. 发出错误

在上一个示例中您可能没有注意到的一件事是我们的 hello.js 文件再次更改。如果我们打开该文件,我们会看到内容仍然与我们的输入文件基本相同。鉴于 tsc 报告了有关我们代码的错误这一事实,这可能有点令人惊讶,但这是基于 TypeScript 的核心价值观之一:很多时候,您会比 TypeScript 更了解。

重申一下前面的内容,类型检查代码限制了您可以运行的程序类型,因此需要权衡类型检查器认为可接受的类型。大多数情况下这没问题,但在某些情况下,这些检查会妨碍您。例如,想象自己将 JavaScript 代码迁移到 TypeScript 并引入类型检查错误。最终你会开始为类型检查器清理一些东西,但原始的 JavaScript 代码已经可以工作了!为什么要将其转换为 TypeScript 会阻止您运行它?

所以 TypeScript 不会妨碍你。当然,随着时间的推移,您可能希望对错误更加防御,并使 TypeScript 的行为更加严格。在这种情况下,您可以使用 noEmitOnError 编译器选项。尝试更改您的 hello.ts 文件并使用该标志运行 tsc:

tsc --noEmitOnError hello.ts

你会注意到 hello.js 永远不会更新。

9.显式类型

到目前为止,我们还没有告诉 TypeScript 什么是人或日期。让我们编辑代码告诉 TypeScript 那个人是一个字符串,那个日期应该是一个 Date 对象。我们还将在日期上使用 toDateString() 方法

function greet(person: string, date: Date) {
  console.log(`Hello ${person}, today is ${date.toDateString()}!`);
}

我们所做的是在 person 和 date 上添加类型注释来描述可以调用什么类型的值。您可以将该签名解读为“greet 接受一个字符串类型的人,以及一个 Date 类型的日期”。

有了这个,TypeScript 可以告诉我们其他可能被错误调用的情况。例如…

嗯? TypeScript 在我们的第二个参数上报告了一个错误,但为什么呢? 

也许令人惊讶的是,在 JavaScript 中调用 Date() 会返回一个字符串。另一方面,用 new Date() 构造一个 Date 实际上给了我们我们所期望的。

无论如何,我们可以快速修复错误:

function greet(person: string, date: Date) {
  console.log(`Hello ${person}, today is ${date.toDateString()}!`);
}
 
greet("Maddison", new Date());

请记住,我们并不总是必须编写明确的类型注释。在许多情况下,TypeScript 甚至可以为我们推断(或“找出”)类型,即使我们省略它们

即使我们没有告诉 TypeScript msg 有类型字符串,它也能弄清楚。这是一个特性,当类型系统最终会推断出相同的类型时,最好不要添加注释。

注意:上面代码示例中的消息气泡。如果您将鼠标悬停在该词上,这就是您的编辑器会显示的内容。

10.擦除类型

我们来看看当我们编译上面的函数greet with tsc输出JavaScript时会发生什么

"use strict";
function greet(person, date) {
    console.log("Hello " + person + ", today is " + date.toDateString() + "!");
}
greet("Maddison", new Date());

这里注意两点: (1)我们的 person 和 date 参数不再有类型注释。 (2)我们的“模板字符串” - 使用反引号(` 字符)的字符串 - 被转换为带有连接 (+) 的纯字符串

稍后会详细介绍第二点,但现在让我们专注于第一点。类型注释不是 JavaScript 的一部分(或者说是学究式的 ECMAScript),因此实际上没有任何浏览器或其他运行时可以不加修改地运行 TypeScript.这就是 TypeScript 首先需要编译器的原因——它需要某种方式来剥离或转换任何 TypeScript 特定的代码,以便您可以运行它。大多数特定于 TypeScript 的代码都被删除了,同样,这里我们的类型注释也被完全删除了。

请记住:类型注释永远不会改变程序的运行时行为。

11.降级

与上面的另一个区别是我们的模板字符串是从

`Hello ${person}, today is ${date.toDateString()}!`;

"Hello " + person + ", today is " + date.toDateString() + "!";

为什么会这样?

模板字符串是来自 ECMAScript 版本的一个特性,称为 ECMAScript 2015(又名 ECMAScript 6、ES2015、ES6 等 - 不要问)。TypeScript 能够将代码从较新版本的 ECMAScript 重写为旧版本,例如 ECMAScript 3 或 ECMAScript 5(又名 ES3 和 ES5)。这种从更新或“更高”版本的 ECMAScript 向下移动到旧或“更低”版本的过程有时称为降级。

默认情况下,TypeScript 以 ES3 为目标,这是一个非常旧的 ECMAScript 版本。我们可以通过使用目标选项来选择更新一些的东西。我们可以通过使用目标选项来选择更新一些的东西。使用 --target es2015 将 TypeScript 更改为目标 ECMAScript 2015,这意味着代码应该能够在支持 ECMAScript 2015 的任何地方运行。所以运行 tsc --target es2015 hello.ts 会给我们以下输出:

function greet(person, date) {
  console.log(`Hello ${person}, today is ${date.toDateString()}!`);
}
greet("Maddison", new Date());

虽然默认目标是 ES3,但当前绝大多数浏览器都支持 ES2015。因此,大多数开发人员可以安全地将 ES2015 或更高版本指定为目标,除非与某些旧浏览器的兼容性很重要。

12.严格

不同的用户使用 TypeScript 在类型检查器中寻找不同的东西。有些人正在寻找更宽松的选择加入体验,它可以帮助仅验证其程序的某些部分,并且仍然拥有不错的工具。这是 TypeScript 的默认体验,其中类型是可选的,推理采用最宽松的类型,并且不检查潜在的 null/undefined 值。就像 tsc 在面对错误时发出的信息一样,这些默认值被放置到位以避开您。如果您正在迁移现有的 JavaScript,这可能是理想的第一步。

相比之下,许多用户更喜欢让 TypeScript 尽可能多地立即验证,这就是该语言也提供严格设置的原因。这些严格性设置将静态类型检查从开关(无论您的代码是否被检查)转变为更接近于拨号的东西。这些严格性设置将静态类型检查从开关(无论您的代码是否被检查)转变为更接近于拨号的东西。如果可能,新的代码库应该始终打开这些严格性检查。

TypeScript 有几个可以打开或关闭的类型检查严格性标志,除非另有说明,否则我们所有的示例都将在启用所有这些标志的情况下编写。CLI 中的 strict 标志或 tsconfig.json 中的“strict”: true 同时将它们全部打开,但我们可以单独选择退出它们。您应该知道的两个最大的问题是 noImplicitAny 和 strictNullChecks。

12.1 noImplicitAny

回想一下,在某些地方,TypeScript 不会尝试为我们推断类型,而是退回到最宽松的类型:any。这并不是可能发生的最糟糕的事情——毕竟,无论如何,回到任何只是简单的 JavaScript 体验。

然而,使用 any 通常首先会破坏使用 TypeScript 的目的。您的程序类型越多,您获得的验证和工具就越多,这意味着您在编写代码时会遇到更少的错误。打开 noImplicitAny 标志将对类型被隐式推断为 any 的任何变量发出错误。

12.2 strictNullChecks

默认情况下,像 null 和 undefined 这样的值可以分配给任何其他类型。这可以使编写一些代码更容易,但忘记处理 null 和 undefined 是世界上无数错误的原因——有些人认为这是一个十亿美元的错误!strictNullChecks 标志使处理 null 和 undefined 更加明确,让我们不必担心是否忘记处理 null 和 undefined。

三 日常类型

1. 原始类型 string number boolean

JavaScript 有三个非常常用的原语:字符串、数字和布尔值。每个在 TypeScript 中都有对应的类型。如您所料,这些名称与您在这些类型的值上使用 JavaScript typeof 运算符时看到的名称相同:

  • 字符串表示字符串值,如“Hello, world”
  • number 表示像 42 这样的数字。 JavaScript 没有一个特殊的整数运行时值,所以没有等价于 int 或 float - 一切都只是数字
  • boolean 用于两个值 true 和 false

类型名称 String、Number 和 Boolean(以大写字母开头)是合法的,但指的是一些很少出现在代码中的特殊内置类型。对于类型,始终使用字符串、数字或布尔值。

2.数组

指定像 [1, 2, 3] 这样的数组的类型,可以使用语法 number[];此语法适用于任何类型(例如 string[] 是一个字符串数组,等等)。你也可以看到它写成 Array<number>,这意味着同样的事情。当我们介绍泛型时,我们将了解更多关于语法 T<U> 的信息。

请注意, [number] 是另一回事;请参阅有关Tuples的部分。

3. any

TypeScript 也有一个特殊的类型,any,当你不希望某个特定的值导致类型检查错误时,你可以使用它。

当一个值是 any 类型时,您可以访问它的任何属性(反过来又是 any 类型),像函数一样调用它,将它分配给(或从)任何类型的值,或几乎任何其他类型这在语法上是合法的:

let obj: any = { x: 0 };
// None of the following lines of code will throw compiler errors.
// Using `any` disables all further type checking, and it is assumed 
// you know the environment better than TypeScript.
obj.foo();
obj();
obj.bar = 100;
obj = "hello";
const n: number = obj;

当你不想写出一个长类型只是为了让 TypeScript 相信特定的代码行没问题时,any 类型很有用。

noImplicitAny

当您不指定类型,并且 TypeScript 无法从上下文中推断出它时,编译器通常会默认为 any。但是,您通常希望避免这种情况,因为 any 未经过类型检查。使用编译器标志 noImplicitAny 将任何隐式 any 标记为错误。

4. 变量上的类型注释

当您使用 const、var 或 let 声明变量时,您可以选择添加类型注释来显式指定变量的类型:

let myName: string = "Alice";

TypeScript 不使用像 int x = 0 这样的“左侧类型”风格的声明;类型注释总是在输入的东西之后。

但是,在大多数情况下,这不是必需的。只要有可能,TypeScript 就会尝试自动推断代码中的类型。例如,根据初始化器的类型推断变量的类型:

// No type annotation needed -- 'myName' inferred as type 'string'
let myName = "Alice";

在大多数情况下,您不需要明确学习推理规则。如果您刚开始,请尝试使用比您想象的更少的类型注释 - 您可能会惊讶于 TypeScript 完全理解正在发生的事情所需的数量如此之少。

5. function

函数是在 JavaScript 中传递数据的主要方式。 TypeScript 允许您指定函数的输入和输出值的类型。

5.1参数类型注释

声明函数时,可以在每个参数后添加类型注解,以声明函数接受的参数类型。参数类型注释在参数名称之后:

// Parameter type annotation
function greet(name: string) {
  console.log("Hello, " + name.toUpperCase() + "!!");
}

当参数具有类型注释时,将检查该函数的参数:

 即使您的参数上没有类型注释,TypeScript 仍会检查您是否传递了正确数量的参数。

5.2 返回类型注释

您还可以添加返回类型注释。返回类型注释出现在参数列表之后:

function getFavoriteNumber(): number {
  return 26;
}

很像变量类型注解,你通常不需要返回类型注解,因为 TypeScript 会根据它的 return 语句推断函数的返回类型。上面例子中的类型注解不会改变任何东西。一些代码库会出于文档目的明确指定返回类型,以防止意外更改,或仅出于个人喜好.

5.3 匿名函数

匿名函数与函数声明略有不同。当一个函数出现在 TypeScript 可以确定它将如何被调用的地方时,该函数的参数会自动被赋予类型.

下面是一个例子:

即使参数 s 没有类型注释,TypeScript 还是使用 forEach 函数的类型以及推断的数组类型来确定 s 将具有的类型。

这个过程称为上下文类型,因为函数发生在其中的上下文通知它应该具有什么类型。

与推理规则类似,您不需要明确了解这是如何发生的,但了解它确实发生可以帮助您注意何时不需要类型注释.稍后,我们将看到更多关于值出现的上下文如何影响其类型的示例。

6. 对象类型 

除了基本类型,你会遇到的最常见的类型是对象类型。这是指任何带有属性的 JavaScript 值,几乎是所有属性!要定义对象类型,我们只需列出其属性及其类型。

例如,这是一个接受点状对象的函数:

// The parameter's type annotation is an object type
function printCoord(pt: { x: number; y: number }) {
  console.log("The coordinate's x value is " + pt.x);
  console.log("The coordinate's y value is " + pt.y);
}
printCoord({ x: 3, y: 7 });

在这里,我们使用具有两个属性(x 和 y)的类型来注释参数,这两个属性都是数字类型。您可以使用 , 或 ;分隔属性,最后一个分隔符是可选的。

每个属性的类型部分也是可选的。如果您不指定类型,则将假定为任何类型。

6.1 可选属性

对象类型还可以指定其部分或全部属性是可选的。为此,添加一个 ?在属性名称之后:

function printName(obj: { first: string; last?: string }) {
  // ...
}
// Both OK
printName({ first: "Bob" });
printName({ first: "Alice", last: "Alisson" });

在 JavaScript 中,如果您访问一个不存在的属性,您将获得 undefined 值而不是运行时错误。因此,当您从可选属性中读取时,您必须在使用它之前检查 undefined。

function printName(obj: { first: string; last?: string }) {
  // Error - might crash if 'obj.last' wasn't provided!
  console.log(obj.last.toUpperCase());
Object is possibly 'undefined'.
  if (obj.last !== undefined) {
    // OK
    console.log(obj.last.toUpperCase());
  }
 
  // A safe alternative using modern JavaScript syntax:
  console.log(obj.last?.toUpperCase());
}

7 联合类型

TypeScript 的类型系统允许您使用多种运算符从现有类型中构建新类型。现在我们知道如何编写几种类型,是时候开始以有趣的方式组合它们了。

7.1 定义一个联合类型

您可能会看到的第一种组合类型的方法是联合类型。联合类型是由两个或多个其他类型组成的类型,表示可能是这些类型中的任何一种的值。我们将这些类型中的每一种称为工会成员。

让我们编写一个可以对字符串或数字进行操作的函数:

7.2 使用联合类型

提供一个匹配联合类型的值很容易——只需提供一个匹配联合任何成员的类型。如果你有一个联合类型的值,你如何使用它?

如果联合的每个成员都有效,TypeScript 将只允许你使用联合做一些事情。例如,如果您有联合字符串 |数字,您不能使用仅适用于字符串的方法:

 

解决方案是用代码缩小联合,就像在没有类型注释的 JavaScript 中一样。当 TypeScript 可以根据代码的结构为值推断出更具体的类型时,就会发生窄化。

例如,TypeScript 知道只有一个字符串值才会有一个 typeof 值“string”:

function printId(id: number | string) {
  if (typeof id === "string") {
    // In this branch, id is of type 'string'
    console.log(id.toUpperCase());
  } else {
    // Here, id is of type 'number'
    console.log(id);
  }
}

 另一个例子是使用像 Array.isArray 这样的函数:

function welcomePeople(x: string[] | string) {
  if (Array.isArray(x)) {
    // Here: 'x' is 'string[]'
    console.log("Hello, " + x.join(" and "));
  } else {
    // Here: 'x' is 'string'
    console.log("Welcome lone traveler " + x);
  }
}

请注意,在 else 分支中,我们不需要做任何特殊的事情——如果 x 不是 string[],那么它一定是一个 string。

有时你会有一个联合类型,所有成员都有一些共同点。例如,数组和字符串都有切片方法。如果联合中的每个成员都有一个共同的属性,则可以使用该属性而不会缩小范围:

// Return type is inferred as number[] | string
function getFirstThree(x: number[] | string) {
  return x.slice(0, 3);
}

类型的联合似乎具有这些类型的属性的交集,这可能会令人困惑。这不是偶然的 - 名称 union 来自类型理论。工会号|字符串是通过取每种类型的值的并集而组成的。请注意,给定两个集合,每个集合都有相应的事实,只有这些事实的交集适用于集合本身的并集。例如,如果我们有一个房间的高个子戴帽子,另一个房间说西班牙语的人戴帽子,将这些房间合并后,我们唯一知道的关于每个人的信息就是他们必须戴帽子。

8 类型别名

我们一直在通过直接在类型注释中编写对象类型和联合类型来使用它们。这很方便,但是想要多次使用同一个类型并用一个名称来引用它是很常见的。

类型别名就是这样 - 任何类型的名称。类型别名的语法是:

type Point = {
  x: number;
  y: number;
};
 
// Exactly the same as the earlier example
function printCoord(pt: Point) {
  console.log("The coordinate's x value is " + pt.x);
  console.log("The coordinate's y value is " + pt.y);
}
 
printCoord({ x: 100, y: 100 });

实际上,您可以使用类型别名为任何类型命名,而不仅仅是对象类型。例如,类型别名可以命名联合类型:

type ID = number | string;

请注意,别名只是别名 - 您不能使用类型别名来创建相同类型的不同/不同“版本”。当您使用别名时,就像您编写了别名类型一样。换句话说,这段代码可能看起来不合法,但根据 TypeScript 是可以的,因为这两种类型都是同一类型的别名:

type UserInputSanitizedString = string;
 
function sanitizeInput(str: string): UserInputSanitizedString {
  return sanitize(str);
}
 
// Create a sanitized input
let userInput = sanitizeInput(getInput());
 
// Can still be re-assigned with a string though
userInput = "new input";

9 interfect

接口声明是另一种命名对象类型的方法:

interface Point {
  x: number;
  y: number;
}
 
function printCoord(pt: Point) {
  console.log("The coordinate's x value is " + pt.x);
  console.log("The coordinate's y value is " + pt.y);
}
 
printCoord({ x: 100, y: 100 });

就像我们使用上面的类型别名一样,这个例子就像我们使用了匿名对象类型一样工作.TypeScript 只关心我们传递给 printCoord 的值的结构——它只关心它是否具有预期的属性。只关心类型的结构和功能是我们将 TypeScript 称为结构类型类型系统的原因。

9.1 类型别名和接口之间的差异

类型别名和接口非常相似,在很多情况下你可以自由选择它们。接口的几乎所有功能都可以在类型中使用,关键区别在于无法重新打开类型以添加​​新属性与始终可扩展的接口。

 10 类型断言

有时,您会获得有关 TypeScript 不知道的值类型的信息。

例如,如果你使用 document.getElementById,TypeScript 只知道这会返回某种 HTMLElement,但你可能知道你的页面总是有一个带有给定 ID 的 HTMLCanvasElement。

在这种情况下,您可以使用类型断言来指定更具体的类型:

const myCanvas = document.getElementById("main_canvas") as HTMLCanvasElement;

与类型注释一样,类型断言由编译器删除,不会影响代码的运行时行为。 您还可以使用尖括号语法(除非代码在 .tsx 文件中),它是等效的:

const myCanvas = <HTMLCanvasElement>document.getElementById("main_canvas");

提醒:因为类型断言在编译时被删除,所以没有与类型断言相关的运行时检查。如果类型断言错误,则不会产生异常或空值。

TypeScript 只允许类型断言转换为更具体或不太具体的类型版本。此规则可防止“不可能”的强制,例如:

有时这条规则可能过于保守,将不允许更复杂的可能有效的强制转换。如果发生这种情况,您可以使用两个断言,首先是 any(或未知,我们将在后面介绍),然后是所需的类型:

const a = (expr as any) as T;

11 文字类型

除了一般类型的字符串和数字外,我们还可以在类型位置引用特定的字符串和数字。

一种思考方式是考虑 JavaScript 如何以不同的方式声明变量.var 和 let 都允许更改变量中保存的内容,而 const 则不允许。这反映在 TypeScript 如何为文字创建类型上。

 

就其本身而言,文字类型并不是很有价值:

拥有一个只能有一个值的变量并没有多大用处!

但是通过将文字组合成联合,你可以表达一个更有用的概念——例如,只接受一组特定已知值的函数:

 数字文字类型的工作方式相同:

 当然,您可以将这些与非文字类型结合使用:

 还有一种文字类型:布尔文字。只有两种布尔文字类型,正如您可能猜到的,它们是 true 和 false 类型。类型 boolean 本身实际上只是 union true | false 的别名。

 12 字面推理

当您使用对象初始化变量时,TypeScript 假定该对象的属性稍后可能会更改值。例如,如果你写了这样的代码:

const obj = { counter: 0 };
if (someCondition) {
  obj.counter = 1;
}

 TypeScript 不认为将 1 分配给以前为 0 的字段是错误的。另一种说法是 obj.counter 必须具有类型号,而不是 0,因为类型用于确定读取和写入行为。

这同样适用于字符串:

在上面的例子中,req.method 被推断为字符串,而不是“GET”.因为代码可以在 req 的创建和 handleRequest 的调用之间进行评估,它可以为 req.method 分配一个像“GUESS”这样的新字符串,TypeScript 认为这段代码有错误。

有两种方法可以解决这个问题。

(1)您可以通过在任一位置添加类型断言来更改推理:

// Change 1:
const req = { url: "https://example.com", method: "GET" as "GET" };
// Change 2
handleRequest(req.url, req.method as "GET");

更改 1 表示“我打算让 req.method 始终具有文字类型“GET””,从而防止之后可能将“GUESS”分配给该字段。更改 2 表示“我知道出于其他原因 req.method 具有值“GET””。

(2)您可以使用 as const 将整个对象转换为类型文字:

const req = { url: "https://example.com", method: "GET" } as const;
handleRequest(req.url, req.method);

 as const 后缀的作用类似于 const,但对于类型系统,确保为所有属性分配文字类型,而不是更通用的版本,如字符串或数字。

13 null undefined

JavaScript 有两个原始值用于表示不存在或未初始化的值:null 和 undefined。

TypeScript 有两个对应的同名类型。这些类型的行为取决于您是否启用了 strictNullChecks 选项。

strictNullChecks off

关闭strictNullChecks 后,仍然可以正常访问可能为null 或undefined 的值,并且可以将值null 和undefined 分配给任何类型的属性。这类似于没有空检查的语言(例如 C#、Java)的行为方式。缺乏对这些值的检查往往是错误的主要来源;如果在他们的代码库中这样做可行,我们总是建议人们打开 strictNullChecks。

strictNullChecks on

启用strictNullChecks 后,当值为空或未定义时,您需要在对该值使用方法或属性之前测试这些值。就像在使用可选属性之前检查 undefined 一样,我们可以使用缩小来检查可能为 null 的值:

function doSomething(x: string | null) {
  if (x === null) {
    // do nothing
  } else {
    console.log("Hello, " + x.toUpperCase());
  }
}

14 非空断言运算符(后缀 ! )

TypeScript 还有一种特殊的语法,可以在不进行任何显式检查的情况下从类型中删除 null 和 undefined。写作 !在任何表达式实际上是一个类型断言之后,该值不为空或未定义:

function liveDangerously(x?: number | null) {
  // No error
  console.log(x!.toFixed());
}

就像其他类型断言一样,这不会改变代码的运行时行为,因此仅使用 !当您知道该值不能为空或未定义时。

15 enums

枚举是 TypeScript 添加到 JavaScript 的一项功能,它允许描述一个值,该值可能是一组可能的命名常量之一。与大多数 TypeScript 功能不同,这不是 JavaScript 的类型级别添加,而是添加到语言和运行时的内容。因此,这是一个您应该知道存在的功能,但除非您确定,否则请不要使用。

枚举是 TypeScript 为数不多的不是 JavaScript 类型级扩展的特性之一。

枚举允许开发人员定义一组命名常量。使用枚举可以更轻松地记录意图,或创建一组不同的案例。 TypeScript 提供数字和基于字符串的枚举。

15.1 数字枚举

我们将首先从数字枚举开始,如果您来自其他语言,可能会更熟悉。可以使用 enum 关键字定义枚举。

enum Direction {
  Up = 1,
  Down,
  Left,
  Right,
}

上面,我们有一个数字枚举,其中 Up 初始化为 1。从那时起,以下所有成员都会自动递增。换句话说,Direction.Up 的值为 1,Down 的值为 2,Left 的值为 3,Right 的值为 4。

如果我们愿意,我们可以完全不使用初始化器:

enum Direction {
  Up,
  Down,
  Left,
  Right,
}

在这里,Up 的值为 0,Down 的值为 1,等等。这种自动递增的行为对于我们可能不关心成员值本身的情况很有用,但确实关心每个值与相同的其他值不同枚举。

使用枚举很简单:只需访问作为枚举本身的属性的任何成员,并使用枚举的名称声明类型:

enum UserResponse {
  No = 0,
  Yes = 1,
}
 
function respond(recipient: string, message: UserResponse): void {
  // ...
}
 
respond("Princess Caroline", UserResponse.Yes);

数字枚举可以混合在计算成员和常量成员中(见下文)。简短的故事是,没有初始化器的枚举要么需要首先,要么必须在用数字常量或其他常量枚举成员初始化的数字枚举之后。换句话说,以下情况是不允许的:

15.2 字符串枚举

字符串枚举是一个类似的概念,但有一些细微的运行时差异,如下所述。在字符串枚举中,每个成员都必须使用字符串文字或另一个字符串枚举成员进行常量初始化。

enum Direction {
  Up = "UP",
  Down = "DOWN",
  Left = "LEFT",
  Right = "RIGHT",
}

 虽然字符串枚举没有自动递增的行为,但字符串枚举的好处是它们可以很好地“序列化”。换句话说,如果您正在调试并且必须读取数字枚举的运行时值,则该值通常是不透明的 - 它本身并没有传达任何有用的含义(尽管反向映射通常会有所帮助),字符串枚举允许您在代码运行时提供有意义且可读的值,而与枚举成员本身的名称无关。

15.3 异构枚举

从技术上讲,枚举可以与字符串和数字成员混合使用,但不清楚为什么要这样做:

enum BooleanLikeHeterogeneousEnum {
  No = 0,
  Yes = "YES",
}

除非您真的想以巧妙的方式利用 JavaScript 的运行时行为,否则建议您不要这样做。

15.4 计算成员和常量成员

每个枚举成员都有一个与之关联的值,该值可以是常量也可以是计算值。在以下情况下,枚举成员被视为常量:

它是枚举中的第一个成员,并且没有初始化器,在这种情况下,它被赋值为 0:

// E.X is constant:
enum E {
  X,
}

它没有初始化器并且前面的枚举成员是一个数字常量。在这种情况下,当前枚举成员的值将是前一个枚举成员的值加一。

// All enum members in 'E1' and 'E2' are constant.
 
enum E1 {
  X,
  Y,
  Z,
}
 
enum E2 {
  A = 1,
  B,
  C,
}

枚举成员使用常量枚举表达式进行初始化。常量枚举表达式是 TypeScript 表达式的子集,可以在编译时完全计算。一个表达式是一个常量枚举表达式,如果它是:

文字枚举表达式(基本上是字符串文字或数字文字)

对先前定义的常量枚举成员的引用(可以源自不同的枚举)

带括号的常量枚举表达式

应用于常量枚举表达式的 +、-、~ 一元运算符之一

+, -, *, /, %, <<, >>, >>>, &, |, ^ 使用常量枚举表达式作为操作数的二元运算符

将常量枚举表达式计算为 NaN 或 Infinity 是编译时错误。

在所有其他情况下,枚举成员被视为已计算.

enum FileAccess {
  // constant members
  None,
  Read = 1 << 1,
  Write = 1 << 2,
  ReadWrite = Read | Write,
  // computed member
  G = "123".length,
}

15.5 联合枚举和枚举成员类型

有一个不计算的常量枚举成员的特殊子集:字面枚举成员。文字枚举成员是一个没有初始化值的常量枚举成员,或具有初始化为

任何字符串文字(例如“foo”、“bar”、“baz”)

任何数字文字(例如 1、100)

应用于任何数字文字的一元减号(例如 -1、-100)

当枚举中的所有成员都具有文字枚举值时,一些特殊的语义就会发挥作用。

首先是枚举成员也成为类型!例如,我们可以说某些成员只能具有枚举成员的值:

enum ShapeKind {
  Circle,
  Square,
}
 
interface Circle {
  kind: ShapeKind.Circle;
  radius: number;
}
 
interface Square {
  kind: ShapeKind.Square;
  sideLength: number;
}
 
let c: Circle = {
  kind: ShapeKind.Square,
Type 'ShapeKind.Square' is not assignable to type 'ShapeKind.Circle'.
  radius: 100,
};

另一个变化是枚举类型本身有效地成为每个枚举成员的联合。使用联合枚举,类型系统能够利用这样一个事实,即它知道枚举本身中存在的确切值集。因此,TypeScript 可以捕获我们可能错误地比较值的错误。例如:

在那个例子中,我们首先检查 x 是否不是 E.Foo。如果检查成功,那么我们的 ||将短路,并且“if”的主体将运行.但是,如果检查不成功,那么x只能是E.Foo,所以看它是否等于E.Bar是没有意义的。

15.6 运行时枚举

枚举是在运行时存在的真实对象。例如,下面的枚举

enum E {
  X,
  Y,
  Z,
}

 实际上可以传递给函数

enum E {
  X,
  Y,
  Z,
}
 
function f(obj: { X: number }) {
  return obj.X;
}
 
// Works, since 'E' has a property named 'X' which is a number.
f(E);

15.7 编译时枚举

尽管枚举是在运行时存在的真实对象,keyof 关键字的工作方式与您对典型对象的预期不同。相反,使用 keyof typeof 获取将所有 Enum 键表示为字符串的 Type。

enum LogLevel {
  ERROR,
  WARN,
  INFO,
  DEBUG,
}
 
/**
 * This is equivalent to:
 * type LogLevelStrings = 'ERROR' | 'WARN' | 'INFO' | 'DEBUG';
 */
type LogLevelStrings = keyof typeof LogLevel;
 
function printImportant(key: LogLevelStrings, message: string) {
  const num = LogLevel[key];
  if (num <= LogLevel.WARN) {
    console.log("Log level key is:", key);
    console.log("Log level value is:", num);
    console.log("Log level message is:", message);
  }
}
printImportant("ERROR", "This is a message");

15.8 反向映射

除了为成员创建具有属性名称的对象之外,数字枚举成员还获得从枚举值到枚举名称的反向映射。例如,在这个例子中:

enum Enum {
  A,
}
 
let a = Enum.A;
let nameOfA = Enum[a]; // "A"
Try

TypeScript 将其编译为以下 JavaScript:

"use strict";
var Enum;
(function (Enum) {
    Enum[Enum["A"] = 0] = "A";
})(Enum || (Enum = {}));
let a = Enum.A;
let nameOfA = Enum[a]; // "A"

在这个生成的代码中,一个枚举被编译成一个存储前向(名称 -> 值)和反向(值 -> 名称)映射的对象。对其他枚举成员的引用始终作为属性访问发出,并且从不内联。

请记住,字符串枚举成员根本不会生成反向映射。

15.9 const enums

在大多数情况下,枚举是一个完全有效的解决方案。然而,有时要求更严格。为了避免在访问枚举值时支付额外生成的代码和额外的间接开销,可以使用 const 枚举。 Const 枚举是在我们的枚举上使用 const 修饰符定义的:

const enum Enum {
  A = 1,
  B = A * 2,
}

常量枚举只能使用常量枚举表达式,并且与常规枚举不同,它们在编译期间被完全删除。 Const 枚举成员在使用站点内联。这是可能的,因为 const 枚举不能有计算成员。

const enum Direction {
  Up,
  Down,
  Left,
  Right,
}
 
let directions = [
  Direction.Up,
  Direction.Down,
  Direction.Left,
  Direction.Right,
];

在生成的代码中将成为

"use strict";
let directions = [
    0 /* Up */,
    1 /* Down */,
    2 /* Left */,
    3 /* Right */,
];

15.10 环境枚举

环境枚举用于描述已经存在的枚举类型的形状。

declare enum Enum {
  A = 1,
  B,
  C = 2,
}

环境枚举和非环境枚举之间的一个重要区别是,在常规枚举中,如果其前一个枚举成员被视为常量,则没有初始化器的成员将被视为常量。相比之下,没有初始化程序的环境(和非常量)枚举成员始终被视为已计算。

15.11 Objects vs Enums

在现代 TypeScript 中,当具有 as const 的对象就足够时,您可能不需要枚举:

const enum EDirection {
  Up,
  Down,
  Left,
  Right,
}
 
const ODirection = {
  Up: 0,
  Down: 1,
  Left: 2,
  Right: 3,
} as const;
 
EDirection.Up;
           
(enum member) EDirection.Up = 0
 
ODirection.Up;
           
(property) Up: 0
 
// Using the enum as a parameter
function walk(dir: EDirection) {}
 
// It requires an extra line to pull out the values
type Direction = typeof ODirection[keyof typeof ODirection];
function run(dir: Direction) {}
 
walk(EDirection.Left);
run(ODirection.Right);

支持这种格式而不是 TypeScript 的枚举的最大论点是,它使您的代码库与 JavaScript 的状态保持一致,并且当/如果将枚举添加到 JavaScript 中时,您可以转向其他语法。

16 不太常见的原始类型

值得一提的是 JavaScript 中在类型系统中表示的其余原始类型。虽然我们不会在这里深入

bigint

从 ES2020 开始,JavaScript 中有一个用于非常大整数的原始类型BigInt

// Creating a bigint via the BigInt function
const oneHundred: bigint = BigInt(100);
 
// Creating a BigInt via the literal syntax
const anotherHundred: bigint = 100n;

symbol

JavaScript 中有一个原语用于通过函数 Symbol() 创建全局唯一引用:

 四 缩小

假设我们有一个名为 padLeft 的函数。

function padLeft(padding: number | string, input: string): string {
  throw new Error("Not implemented yet!");
}

如果 padding 是一个数字,它会将其视为我们想要在输入前添加的空格数。如果 padding 是一个字符串,它应该只是在输入之前添加 padding。让我们尝试实现向 padLeft 传递一个数字进行填充时的逻辑。

哦,哦,我们在填充时遇到错误。 TypeScript 警告我们将数字添加到数字 | string 可能不会给我们想要的东西,这是对的。换句话说,我们没有先明确检查 padding 是否是数字,也没有处理它是字符串的情况,所以让我们这样做。 

function padLeft(padding: number | string, input: string) {
  if (typeof padding === "number") {
    return " ".repeat(padding) + input;
  }
  return padding + input;
}

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值