TypeScript:JavaScript + 类型 = 卓越的开发人员生产力

2025-06-07

TypeScript:JavaScript + 类型 = 卓越的开发人员生产力

本文是JavaScript-mancy OOP 的一章:掌握 JavaScript 中召唤物体的神秘艺术,这是一个令人惊叹的史诗级故事,带有尴尬的幽默感,并且通过一次使用一个 JavaScript 函数来拯救世界。

JavaScript-mancy is a dangerous art.
Get an incantation slightly wrong,
and anything can happen. 

More than one young apprentice
has found out about this the hard way,
just a split second before 
razing a village to the ground,
letting loose a bloodthirsty beast,
or making something unexpected explode.

That's generally the way it is.

There are records of an ancient order,
an order of disciplined warrior monks,
who were able to tame the wild winds of magic.
But too little of them remains.

Did they exist? 
Or are they just wishful thinking and myth?

        - The Chronicler
Enter fullscreen mode Exit fullscreen mode
/* 
The group walks into the chamber and 
the door closes itself behind them...

...the chamber is poorly lit. A metal brazier of eerie blue 
flames lies in the middle of the room and bathes a strange 
obsidian obelisk in a mysterious light. Huge columns 
surround the obelisk at irregular intervals. 
Under the dim light it is impossible to ascertain 
the room proportions...
*/

mooleen.says('Something about this feels very wrong...');
red.says("I couldn't agree more");

randalf.says("Well, it's either destroy that totem and " + 
  "escape aided by magic or go back and fight The Red Legion" +
  "with our bare fists");
rat.says("...and sharp claws");

mooleen.says("Alright, let's get this over with");

/* 
  The group approaches the obelisk under an oppressive
  silence only broken by the sound of boots scraping 
  the sand covered ground.
*/

red.says('Oh shit');
mooleen.says('Oh shit? I thought you ' +
  'were beyond that type of language');

/* 
  Eerie blue and green lights start flaring around the
  group inundating the chamber with light and a ghastly 
  atmosphere. Up and up they go to reveal enormous terraces 
  filled with dark winged figures. A voice suddenly booms 
  within the chamber:
*/

voice.thunders('Let the games begin!');
voice.thunders("In our next game we'll recreate " + 
  "The Fall of the Order of The Red Moon " + 
  "the sacred order of warrior monks" + 
  "whose fierceness still echoes across the centuries");
voice.thunders('I give you: The Last Stand!');

/* 
A thunderous applause mixed with screeches, screams and 
shouts of joy and excitement follows the proclamation.
At the same time 4 humongous iron doors start slowly
opening and row after row of obscene four-legged reptilian 
creatures start emerging from them. Their impossibly huge 
mandibles and terrible wails freeze your blood.
*/

mooleen.says('Oh shit');

voice.thunders('The rules of the game:');
voice.thunders('#1. Fight or Die...');
voice.thunders('#2. You Shall Only Use Types!');
voice.thunders('#3. Only One Shall Remain');

你应该只使用类型!

恭喜您读完了本书!我为您准备了一份特别的礼物:TypeScript!TypeScript 在过去几年中发展势头强劲,在 .NET 领域内外都得到了广泛的应用,甚至在 Angular 和 React 等流行的前端框架中也得到了广泛的应用。TypeScript 提供了目前 Web 上最接近 C# 的体验。尽情享受吧!

JavaScript + 类型 = 卓越的开发效率

TypeScript 是 JavaScript 的超集,它在 JavaScript 之上添加了类型注释,从而实现了静态类型。

如果您是 C# 或 Java 开发者,编写 TypeScript 会非常轻松。如果您是 JavaScript 开发者或有动态编程语言的背景,那么您会遇到一个稍微冗长的 JavaScript 版本,它可以带来更安全、更优质的开发者体验。无论如何,您都会很高兴地知道,您迄今为止学到的关于 JavaScript 的一切也适用于 TypeScript,也就是说,任何 JavaScript 都是有效的 TypeScript

任何 JavaScript 都是有效的 TypeScript

实验 JavaScriptmancer!!

您可以使用 TypeScript 游乐场或从GitHub下载源代码来试验本节中的示例

任何 JavaScript 代码都是有效的 TypeScript。假设我们有一段你能编写的最基本的 JavaScript 代码,一个代表你的魔法值储备的简单变量声明:

var manaReserves = 10;
Enter fullscreen mode Exit fullscreen mode

现在假设我们想通过喝魔法药水来补充你的法力储备:

function rechargeMana(potion){
  return potion.manaModifier * (Math.floor(Math.rand()*10) + 1);
}
Enter fullscreen mode Exit fullscreen mode

因此我们开始写下以下内容:

manaReserves += rechargeMana({
  name: 'light potion of mana',
  manaModifier: 1.5 
});
Enter fullscreen mode Exit fullscreen mode

当我们执行上面的代码时,会出现以下错误:

// => Uncaught TypeError: Math.rand is not a function
Enter fullscreen mode Exit fullscreen mode

这很有道理,因为 JavaScript 中没有Math.rand函数这种东西。它被称为Math.random。不知何故,我把这个函数和一个用途相同、名称略有不同的 C 函数混用了,那个函数是我学生时代用过的。不管怎样,我还是一次又一次地犯这个错误。

上面的代码是一段非常传统的 JavaScript 代码。但它也是完全有效的 TypeScript,只有一点不同。如果rechargeMana用 TypeScript 编写,会自动导致编译器错误,如下所示:

Property 'rand' does not exist on type 'Math'.
Enter fullscreen mode Exit fullscreen mode

这会立即提醒我(再次)犯了一个错误,并且我能够在执行程序之前修复它。这是 TypeScript 的优势之一:更短的反馈循环,你可以在编译时而不是运行时检测代码中的错误

让我们扩展前面的例子并喝另一种药水:

rechagreMana({
  name: 'Greater Potion of Mana',
  manaModifier: 2
})
Enter fullscreen mode Exit fullscreen mode

再次。一个简单的拼写错误,JavaScript 中的一个经典错误,会导致运行时出现ReferenceError,但 TypeScript 编译器会立即捕获它:

Cannot find name 'rechagreMana'.
Enter fullscreen mode Exit fullscreen mode

正如我们目前所见,TypeScript 编译器位于您编写的 TypeScript 代码和浏览器中运行的输出之间,它可以在原生 JavaScript 上为您做很多事情。但真正发挥作用的是添加类型注解,即在 JavaScript 代码中添加与类型相关的额外信息。

例如,让我们rechargeMana用一些类型注释来更新我们的原始函数:

function rechargeMana(potion: { manaModifier : number }) {
  return potion.manaModifier * (Math.floor(Math.random()*10) + 1);
}
Enter fullscreen mode Exit fullscreen mode

上面的示例包含potion参数的类型注释{manaModifier : number}。此注释表示该参数应为具有类型potion属性的对象manaModifiernumber

类型注释为我们做了几件事:

  1. 当作为参数传递的对象rechargeMana没有预期的接口时,它可以帮助编译器发现错误。也就是说,当它缺少manaModifier函数运行所必需的属性时。
  2. potion当您在函数主体内使用该对象时,它可以帮助编译器发现拼写错误或类型错误。
  3. potion它让我们在函数内部键入时能够自动完成语句,rechargeMana这对于开发人员来说是一个很棒的体验1。如果您不熟悉语句完成,它包含一些有用的编辑器内信息,这些信息会弹出并告诉您如何使用对象,例如哪些属性和方法可用,不同参数的预期类型等。

让我们用一个例子来说明1)。假设除了法力药水之外,你还有力量药水:

const potionOfStrength = {
  name: 'Potion of Strength',
  strengthModifier: 3,
  duration: 10
};
Enter fullscreen mode Exit fullscreen mode

在我们的程序中的某个时刻,我们可能会错误地调用此代码:

rechargeMana(potionOfStrength);
Enter fullscreen mode Exit fullscreen mode

使用 as 参数调用该rechargeMana函数potionOfStrength会导致 JavaScript 出现运行时错误,甚至可能导致难以捉摸的错误,因为乘以undefinedanumber会导致崩溃NaN而不是直接崩溃。

然而,在 TypeScript 中,上面的示例会导致以下编译器错误:

// [ts] 
// Argument of type '{ name: string; strengthModifier: number; }' 
// is not assignable to parameter 
//   of type '{ manaModifier: number; }'.
// Property 'manaModifier' is missing 
// in type '{ name: string; strengthModifier: number; }'.
Enter fullscreen mode Exit fullscreen mode

这个错误会很快告诉我,力量药水缺少使用所需的契约rechargeMana,这样就能立刻省去我大量的眼泪和沮丧。另外,请花点时间欣赏一下上面错误信息的质量和精准度。

因此,任何 JavaScript 都是有效的 TypeScript。将你的code.js文件转换为code.tsTypeScript 文件,并用 TypeScript 编译器运行它,TypeScript 会尝试从你的代码中推断出尽可能多的信息,并尽力帮助你。在此基础上添加类型注解,TypeScript 将能够更深入地了解你的代码和意图,并为你提供更好的支持。

那么,TypeScript 的优点和缺点是什么?

通过使用新功能、类型注释和静态类型增强 JavaScript,TypeScript 提供了以下优势:

  • 更强大的错误检测功能。TypeScript 可以对代码进行静态分析,并在实际运行代码之前发现错误。这大大缩短了反馈循环,以便您在编辑器中发现错误时就能立即修复,而不是等到它们进入生产环境后再进行修复。
  • 更好的工具和开发人员生产力。丰富的类型信息可供编辑器和 IDE 使用,提供出色的工具来提高开发人员的生产力,例如编辑器内编译器警告、语句完成、安全重构、内联文档等…… Vis​​ual Studio Code是一款开箱即用、具有出色 TypeScript 支持的文本编辑器。
  • 出色的 API 可发现性。使用类型注释提供的语句补全功能,是在编辑器中直接发现新 API 的绝佳方法。
  • 编写更具目的性的代码。TypeScript 类型注解和访问级别关键字等附加功能,允许您限制所设计的 API 的使用方式。这让您能够编写更具目的性的代码。
  • ESnext 功能。TypeScript 支持许多 ESnext 功能,例如类成员、装饰器等async/await
  • 额外的 TypeScript 功能。除了 JavaScript 和 ESnext 功能外,TypeScript 还具有少量 ECMA-262 规范中没有的功能,这些功能为语言增添了许多功能,例如属性访问级别和参数属性。
  • 与第三方库兼容。在应用程序代码中使用类型注释非常棒,但是您在整个应用程序代码中使用和引用的所有第三方库该怎么办呢?TypeScript 如何与它们交互?特别是,当这些库不是用 TypeScript 编写时会发生什么?在最坏的情况下,TypeScript 会将它不知道的对象视为类型,any这基本上意味着“这个对象可以是任何形状,因此只需像在 JavaScript 中一样操作,不要做任何假设”。更常见的是,第三方库附带提供 TypeScript 类型信息的声明文件,或者您可以通过DefinitelyTyped 项目(TypeScript 类型定义存储库)找到这些声明文件。这意味着您将能够享受与您自己的代码相同级别(甚至更高)的第三方库 TypeScript 支持。
  • 非常适合大型应用程序和团队。TypeScript 擅长支持拥有大型应用程序的多个团队。类型注释和 TypeScript 编译器非常擅长捕捉重大变更、细微错误以及新 API 的可发现性。

缺点是:

  • TypeScript 需要一个转译步骤。TypeScript 代码并非在任何浏览器中都支持。为了能够使用 TypeScript 编写应用程序,您需要设置某种构建管道,将 TypeScript 代码转译为可在浏览器中运行的 JavaScript 版本。幸运的是,开源社区对此提供了强大的支持,您可以在最流行的框架和构建工具中找到对 TypeScript 的出色集成。
  • 你需要学习类型注解的语法和相关构件。类型注解、它们的语法以及相关的构件(例如接口、泛型等)会在你编写 JavaScript 应用程序所需了解的所有内容之上,增加更多的认知负担和额外的复杂性。
  • 它很冗长。添加类型注释会使你的 JavaScript 代码更加冗长(call(person:Person)),这在美观上可能很不美观(尤其是在刚开始的时候)。TypeScript 编译器在推断类型和将你需要编写的类型注释数量降至最低方面做得很好,但为了充分利用 TypeScript,你需要自己添加相当多的类型注释。
  • 它有时会与 ECMAScript 标准不一致。今天就将 ESnext 功能带给您,虽然很棒,但也存在缺点。在 ESnext 功能正式化之前实现它们可能会导致 TypeScript 违反标准,就像模块一样。幸运的是,TypeScript 作为 JavaScript 超集的核心理念促使 TypeScript 团队实现了对 ES6 模块的支持,并弃用了非标准版本。这很好地表明了 TypeScript 对 JavaScript 的忠诚,但在采用 ESnext 功能时仍然值得考虑。

设置一个简单的 TypeScript 项目

想要了解完整的 TypeScript 开发体验,最好的方法是从头开始搭建一个简单的 TypeScript 项目,然后跟着学习本章的剩余部分。和往常一样,你可以从GitHub下载这些项目以及所有示例的源代码。

最简单的入门方法是在你的开发电脑上安装node 和 npm。完成后,我们将使用 npm 安装 TypeScript 编译器:

$ npm install -g typescript
Enter fullscreen mode Exit fullscreen mode

您可以通过运行以下命令来验证安装是否正确运行:

$ tsc -v
Version 2.4.2
Enter fullscreen mode Exit fullscreen mode

并访问 TypeScript 编译器帮助:

$ tsc -h
Version 2.4.2
Syntax:   tsc [options] [file ...]

Examples: tsc hello.ts
          tsc --outFile file.js file.ts
          tsc @args.txt
Enter fullscreen mode Exit fullscreen mode

在这些示例中我将使用Visual Studio Code,但欢迎您使用任何您喜欢的编辑器

在下面输入此命令将创建一个名为的新 TypeScript 文件hello-wizard.ts,并在 Visual Studio Code 上打开它:

$ code hello-wizard.ts
Enter fullscreen mode Exit fullscreen mode

让我们用 TypeScript 中的函数编写规范的 hello 向导sayHello

function sayHello(who: string) : void {
  console.log(`Hello ${who}! I salute you JavaScript-mancer!`);
}
Enter fullscreen mode Exit fullscreen mode

string注意我们如何为该函数的参数添加类型注释who。如果我们尝试使用与预期类型不匹配的参数调用该函数,string编译器将在编辑器中发出编译器错误警告:

sayHello(42);
// => [ts] Argument of type '42' is not assignable 
//         to parameter of type 'string'.
Enter fullscreen mode Exit fullscreen mode

让我们通过向自己致敬来解决这个问题。更新上面的代码,将你的名字包含在字符串中:

sayHello('<Your name here>');
Enter fullscreen mode Exit fullscreen mode

现在,您可以使用终端内的编译器编译 TypeScript 文件(Visual Studio 附带一个嵌入式终端,您可以在编辑器内运行,非常方便)。输入:

$ tsc hello-world.ts
Enter fullscreen mode Exit fullscreen mode

这将告诉 TypeScript 编译器将你的 TypeScript 应用程序转换为可以在浏览器中运行的 JavaScript。它将生成一个hello-world.js包含以下代码的原生 JavaScript 文件:

function sayHello(who) {
  console.log("Hello " + who + "! I salute you JavaScript-mancer!");
}
sayHello('<Your name here>');
Enter fullscreen mode Exit fullscreen mode

漂亮的原生 JavaScript,就像你亲手写出来的一样。你可以用它node来运行这个文件:

$ node hello-world.js
Hello <Your name here>! I salute you JavaScript-mancer!
Enter fullscreen mode Exit fullscreen mode

瞧!你已经编写、编译并运行了你的第一个 TypeScript 程序!世界来了!

由于每次修改ts文件时都运行 TypeScript 编译器可能有点繁琐,因此您可以将编译器设置为监视模式。这将告诉 TypeScript 编译器监视您的源代码文件,并在检测到更改时对其进行转译。要将 TypeScript 编译器设置为 监视模式 ,只需输入以下命令:

$ tsc -w hello-world.ts
10:55:11 AM - Compilation complete. Watching for file changes.
Enter fullscreen mode Exit fullscreen mode

在接下来的部分中,我们将发现 TypeScript 中可以使用的一些强大功能、您需要了解的有关 TypeScript 类型注释的所有信息以及在实际项目中使用 TypeScript 时需要考虑的事项。

Visual Studio Code 与 TypeScript 配合良好!

如果您想了解有关如何使用 TypeScript 在 Visual Studio Code 中进行出色设置的更多信息,我建议您查看本指南

很酷的 TypeScript 功能

除了类型注释之外,TypeScript 还通过 ESnext 功能和一些自己的功能对 JavaScript 进行了改进。

TypeScript 为您带来许多 ESnext 功能

本节中我们将看到的许多功能都是 ESnext 的功能,这些功能是处于不同成熟度级别的提案。您可以在TC39 ECMA-262 GitHub 仓库中找到有关当前正在审议的所有提案的更多信息。

其中一些功能在使用带有实验性标志的 Babel 时也可用。微软内部有专门的团队维护 TypeScript,这让您在 TypeScript 中使用这些功能时充满信心。

TypeScript 类

TypeScript 类具有一些特性,可以提供比 ES6 类更好的开发者体验。第一个特性是类成员

不要像这样编写你的课程:

// ES6 class
class Gladiator {
  constructor(name, hitPoints){
    this.name = name;
    this.hitPoints = hitPoints;
  }
  toString(){
    return `${this.name} the gladiator`
  }
}
Enter fullscreen mode Exit fullscreen mode

您可以提取类成员name并将hitPoints其提取到类主体中,就像在静态类型语言中一样:

class Gladiator {
  name: string;
  hitPoints: number;

  constructor(name: string, hitPoints: number){
    this.name = name;
    this.hitPoints = hitPoints;
  }

  toString(){
    return `${this.name} the gladiator`
  }
}
Enter fullscreen mode Exit fullscreen mode

这可能稍微冗长,因此 TypeScript 附带了另一个称为参数属性的功能,该功能允许您指定类成员并通过构造函数一次性初始化它。

与上述使用参数属性的版本等效的版本如下所示:

class SleekGladiator {
  constructor(public name: string, 
              public hitPoints: number){}

  toString(){
    return `${this.name} the gladiator`
  }
}
Enter fullscreen mode Exit fullscreen mode

是不是更好?public类构造函数中的关键字 告诉 TypeScriptnamehitPoints是可以通过构造函数初始化的类成员。

此外,该public关键字还提示了 TypeScript 为类带来的最后一项改进:访问修饰符。TypeScript 附带四个访问修饰符,用于确定如何访问类成员:

  • readonly:使成员变为只读。必须在声明时或在构造函数中初始化它,之后无法更改。
  • private:将成员设为私有。它只能在类内部访问。
  • protected:使成员受保护。它只能在类或派生类型内部访问。
  • public:将成员设置为公开。任何人都可以访问它。遵循 JavaScript ES6 类实现,public如果没有指定,则是类成员和方法的默认访问修饰符。

修饰符使我们无需像前几章那样readonly定义装饰器。@readonly

一旦给出名字,就不能更改,因此让我们将Gladiator名字设为只读:

class FixedGladiator {

  constructor(readonly name: string,
              public hitPoints: number){}

  toString(){
    return `${this.name}, the gladiator`
  }

}
Enter fullscreen mode Exit fullscreen mode

现在,当我们创建一个新的角斗士并给他或她起一个名字时,它会被刻在石头上:

const maximo = new FixedGladiator('Maximo', 5000);

maximo.name = "Aurelia";
// => [ts] Cannot assign to 'name' because it is 
//         a constant or a read-only property.
Enter fullscreen mode Exit fullscreen mode

这里需要注意的是,这些访问修饰符仅适用于 TypeScript。也就是说,当你编写 TypeScript 代码时,TypeScript 编译器会强制使用它们,但当你的代码被编译成 JavaScript 时,它们会被删除。

上述代码的转换版本FixedGladiator结果如下:

var FixedGladiator = (function () {

  function FixedGladiator(name, hitPoints) {
    this.name = name;
    this.hitPoints = hitPoints;
  }

  FixedGladiator.prototype.toString = function () {
    return this.name + ", the gladiator";
  };

  return FixedGladiator;
}());
Enter fullscreen mode Exit fullscreen mode

从上面的例子可以看出,没有任何机制可以确保该name属性是只读的。

接下来让我们测试一下private访问修饰符。在前面的章节中,我们讨论了在 JavaScript 中实现数据隐私的不同方法:闭包和符号。在 TypeScript 中,你可以使用private(和protected)访问修饰符来实现数据隐藏。

这是我们在第 6 章“白塔召唤增强:ES6 类的奇迹”中使用的例子,展示了使用闭包进行数据隐藏:

class PrivateBarbarian {

  constructor(name){
    // private members
    let weapons = [];
    // public members
    this.name = name;
    this["character class"] = "barbarian";
    this.hp = 200;

    this.equipsWeapon = function (weapon){ 
      weapon.equipped = true;
      // the equipsWeapon method encloses the weapons variable
      weapons.push(weapon);
      console.log(`${this.name} grabs a ${weapon.name} ` + 
                  `from the cavern floor`);
    };
    this.toString = function(){
      if (weapons.length > 0) {
        return `${this.name} wields a ` + 
               `${weapons.find(w => w.equipped).name}`;
      } else return this.name
    };
  }

  talks(){ 
    console.log("I am " + this.name + " !!!");
  }

  saysHi(){ 
    console.log("Hi! I am " + this.name);
  }
};
Enter fullscreen mode Exit fullscreen mode

在这个例子中,我们使用闭包来封装weapons变量,该变量在所有情况下都变为私有的。正如你所理解的,使用闭包迫使我们将使用该变量的方法equipsWeapon和方法从类的主体移到了构造函数的主体中。toStringweapons

TypeScript 中此类的等效项如下所示:

class PrivateBarbarian {
  // private members
  private weapons = [];

  // public members
  ["character class"] = "barbarian";
  hp = 200;

  constructor(public name: string) {}

  equipsWeapon(weapon) { 
    weapon.equipped = true;
    // the equipsWeapon method encloses the weapons variable
    this.weapons.push(weapon);
    console.log(`${this.name} grabs a ${weapon.name} ` + 
                `from the cavern floor`);
  }

  toString() {
    if (this.weapons.length > 0) {
    return `${this.name} wields a ` + 
            `${this.weapons.find(w => w.equipped).name}`;
    } else return this.name
  };

  talks(){ 
    console.log("I am " + this.name + " !!!");
  }

  saysHi(){ 
    console.log("Hi! I am " + this.name);
  }
};
Enter fullscreen mode Exit fullscreen mode

如果您现在实例化一个不屈不挠的野蛮人并尝试访问该weapons属性,您将会看到以下错误:

const conan = new PrivateBarbarian("shy Conan");
// const privateWeapons = conan.weapons;
// => [ts] Property 'weapons' is private and 
//         only accessible within class 'PrivateBarbarian'.
Enter fullscreen mode Exit fullscreen mode

如果你回顾并比较这两种方法,我想你会同意我的观点:TypeScript 语法比 ES6 语法更易读。将所有方法都放在类的主体内,比将方法拆分到两个不同的地方更一致,也更容易理解。

另一方面,TypeScript 的private访问修饰符是 TypeScript 的一个特性,当代码被编译成 JavaScript 时,它会消失。也就是说,原本可以访问输出 JavaScript 的库使用者将能够访问weapons此类的属性。这通常不会造成问题,因为很可能整个开发团队都会使用 TypeScript,但在某些情况下,这可能会造成问题。例如,我认为对于那些使用 TypeScript 创建库并使其可供使用原生 JavaScript 的使用者访问的库创建者来说,这就会是一个问题。

为什么我在编写 ES6 类时会收到 TypeScript 错误?这不是有效的 JavaScript 代码吗?

好问题!当你在所选的 TypeScript 编辑器中输入包含 ES6 类的代码示例时,Barbarian你会惊讶地发现this.namethis.hpthis.equipsWeapon声明会导致 TypeScript 编译器错误。怎么回事?我以为所有 JavaScript 代码都是有效的 TypeScript,而这段代码完全是有效的 ES6 代码。这是怎么回事?我一直在撒谎吗?

出现这些错误的原因是 TypeScript 具有不同级别的正确性:

  • 在第一级,TypeScript 编译器会在应用类型注解之前检查代码的语法是否正确。如果正确,它就能进行转译并生成正确的 JavaScript 代码(我们刚刚发现的 ES6 类问题就是这种情况)。
  • 在第二级,TypeScript 编译器会检查类型注释。根据 TypeScript 的类型系统,它PrivateBarbarian没有任何属性name(属性是在类的主体内声明的),因此会显示错误[ts] Property 'name' does not exist on type 'PrivateBarbarian'
  • 在通过编译器标志启用的第三级中,--noImplicitAnyTypeScript 编译器将变得非常严格,并且不会假定未注释变量的类型是any。也就是说,它将要求所有变量、属性和方法都必须有类型。

因此,在我们的 ES6 示例中,TypeScript 将您的代码理解为有效的 ES6,并且能够将您的代码转换为 JavaScript,但根据 TypeScript 的类型系统,您应该重构您的类并将类成员移动到类主体内。

枚举

TypeScript 的另一个很棒的特性是枚举。枚举是 C# 和 Java 等静态类型语言中的常见数据类型,用于以强类型方式表示有限数量的事物。

假设你想表达所有不同的元素魔法流派:火、水、气和土。当你创造不同的元素法术时,它们会属于其中几个流派,并且相对于其他流派的法术来说各有优缺点。例如,一个火球法术可以像这样:

const fireballSpell = {
  type: 'fire',
  damage: 30,
  cast(target){
    const actualDamage = target.inflictDamage(this.damage, 
                                              this.type);
    console.log(`A huge fireball springs from your ` +  
        `fingers and impacts ${target} (-${actualDamage}hp)`);
  }
};
Enter fullscreen mode Exit fullscreen mode

他们target.inflictDamageactualDamage根据目标对特定元素魔法的抵抗力或是否有针对该魔法的保护法术来计算对目标造成的伤害。

这个例子的问题在于,字符串本身并没有刻意设计,也没有提供太多关于元素魔法学派的信息。在上面的例子中,很容易出现拼写错误,'fire'导致字符串拼写错误。

对以前的方法的改进是使用对象来封装所有可用的选项:

const schoolsOfElementalMagic = {
  fire: 'fire',
  water: 'water',
  air: 'air',
  earth: 'earth'
};
Enter fullscreen mode Exit fullscreen mode

现在我们可以重写前面的例子:

const fireballSpell = {
  type: schoolsOfElementalMagic.fire,
  damage: 30,
  cast(target){
    const actualDamage = target.inflictDamage(this.damage, 
                                              this.type);
    console.log(`A huge fireball springs from your ` +  
        `fingers and impacts ${target} (-${actualDamage}hp)`);
  }
};
Enter fullscreen mode Exit fullscreen mode

太棒了!这比我们之前用的魔法字符串好多了。不过它仍然容易出错,而且没有什么能阻止你type: 'banana'在咒语里写字。

这就是 TypeScript 枚举的用武之地。它提供了一种静态且强类型的方式来表示有限的事物或状态集合。SchoolsOfMagic枚举可能如下所示:

enum SchoolsOfMagic {
  Fire,
  Water,
  Air,
  Earth
}
Enter fullscreen mode Exit fullscreen mode

这个枚举允许我们指定一个表示 a 形状的接口Spell。注意 validSpell有一个type属性,其类型是我们刚刚创建的枚举SchoolsOfMagic

// now we can define a Spell interface
interface Spell {
  type: SchoolsOfMagic,
  damage: number,
  cast(target: any);
}
Enter fullscreen mode Exit fullscreen mode

接口?

接口是 TypeScript 的另一个新功能。它允许你定义任意类型,从而编写更具针对性的代码并丰富你的开发体验。我们将在本章后面详细了解接口。

现在,当我们定义一个新的咒语时,TypeScript 将强制要求该type咒语所提供的类型为SchoolsOfMagic,不仅如此,当使用 Visual Studio Code 等编辑器时,它将通过语句完成为我们提供所有可用的选项(、、FireWaterAirEarth

const enumifiedFireballSpell: Spell = {
  type: SchoolsOfMagic.Fire,
  damage: 30,
  cast(target){
    const actualDamage = target.inflictDamage(this.damage, 
                                              this.type);
    console.log(`A huge fireball springs from your ` +  
        `fingers and impacts ${target} (-${actualDamage}hp)`);
  }
}
Enter fullscreen mode Exit fullscreen mode

如果我们输入除SchoolOfMagic枚举之外的其他任何内容(例如字符串),TypeScript 会立即使用以下错误消息警告我们:

// providing other than a SchoolsOfMagic enum would result in error:
// [ts] 
//   Type 
//  '{ type: string; damage: number; cast(target: any): void; }' 
//   is not assignable to type 'Spell'.
//   Types of property 'type' are incompatible.
//   Type 'string' is not assignable to type 'SchoolsOfMagic'.
Enter fullscreen mode Exit fullscreen mode

当转换为 JavaScript 枚举时,将产生以下代码:

var SchoolsOfMagic;
(function (SchoolsOfMagic) {
    SchoolsOfMagic[SchoolsOfMagic["Fire"] = 0] = "Fire";
    SchoolsOfMagic[SchoolsOfMagic["Water"] = 1] = "Water";
    SchoolsOfMagic[SchoolsOfMagic["Air"] = 2] = "Air";
    SchoolsOfMagic[SchoolsOfMagic["Earth"] = 3] = "Earth";
})(SchoolsOfMagic || (SchoolsOfMagic = {}));
Enter fullscreen mode Exit fullscreen mode

乍一看,它可能有点令人望而生畏。但让我们将其分解成更小的语句:

// Set 'Fire' property in SchoolsOfMagic to 0
SchoolsOfMagic["Fire"] = 0;

// it evaluates to 0 so that this:
SchoolsOfMagic[SchoolsOfMagic["Fire"] = 0] = "Fire";
// is equivalent to:
SchoolsOfMagic[0] = "Fire";
// which means set '0' property in SchoolsOfMagic to "Fire"
Enter fullscreen mode Exit fullscreen mode

因此,枚举通过枚举名称表示数字和字符串之间的双向映射。就像可以指定名称一样,也可以在声明枚举时选择数字:

// Start in 1 and increase numbers
enum SchoolsOfMagic {
  Fire=1,
  Water,
  Air,
  Earth
}

// Explicitly set all numbers
enum SchoolsOfMagic {
  Fire=2,
  Water=4,
  Air=6,
  Earth=8
}

// Computed enums
enum SchoolsOfMagic {
  Fire=1,
  Water=Fire*2,
  Air=2,
  Earth=Air*2
}
Enter fullscreen mode Exit fullscreen mode

当我们不希望转译后的 JavaScript 代码包含对枚举的引用时(例如,在受限的环境中,我们想要减少代码量),我们可以使用const枚举。以下枚举定义不会被转译成 JavaScript:

const enum SchoolOfMagic {
  Fire,
  Water,
  Air,
  Earth
}
Enter fullscreen mode Exit fullscreen mode

相反,它将被内联,并且任何对FireWater和的引用AirEarth将被替换为数字。在本例中分别为 0、1、2、3。

还是喜欢字符串?看看这个字符串字面量类型

如果你仍然喜欢原生字符串,TypeScript 能够基于一系列特定的有效字符串创建类型。我们魔法学校的对应代码如下:

type SchoolsOfMagic = "fire" | "earth" | "air" | "water";
Enter fullscreen mode Exit fullscreen mode

我们再次根据这个新类型定义一个接口:

interface Spell {
  type: SchoolsOfMagic,
  damage: number,
  cast(target: any);
}
Enter fullscreen mode Exit fullscreen mode

现在我们已经准备好创建咒语了。使用任何非允许的字符串都会导致转换错误:

const FireballSpell: Spell = {
  type: "necromancy", 
  damage: 30,
  cast(target){
    const actualDamage = target.inflictDamage(this.damage, this.type);
    console.log(`A huge fireball springs from your ` +  
        `fingers and impacts ${target} (-${actualDamage}hp)`);
  }
}
// => [ts] 
//  Type '{ type: "necromancy"; damage: number; 
//          cast(target: any): void; }' 
//  is not assignable to type 'SpellII'.
//  Types of property 'type' are incompatible.
//  Type '"necromancy"' is not assignable to type 'SchoolsOfMagicII'.
Enter fullscreen mode Exit fullscreen mode

对象展开和静止

JavaScript-mancy:入门中,我们看到了ES6 带来的剩余参数扩展运算符。

你可能还记得,剩余参数提升了开发者使用多个参数2声明函数的体验。arguments我们不再像 ES6 之前那样使用对象:

function obliterate(){
  // Unfortunately arguments is not an array :O
  // so we need to convert it ourselves
  var victims = Array.prototype.slice.call(arguments, 
                              /* startFromIndex */ 0);

  victims.forEach(function(victim){
    console.log(victim + " wiped off of the face of the earth");
  });
  console.log('*Everything* has been obliterated, ' + 
              'oh great master of evil and deceit!');
}
Enter fullscreen mode Exit fullscreen mode

我们可以使用 rest 语法将所有传入的参数直接收集到数组中victims

function obliterate(...victims){
  victims.forEach(function(victim){
    console.log(`${victim} wiped out of the face of the earth`);
  });
  console.log('*Everything* has been obliterated, ' + 
              'oh great master of evil and deceit!');
}
Enter fullscreen mode Exit fullscreen mode

另一方面,扩展运算符的工作方式与剩余参数相反。它不是接受可变数量的参数并将它们打包到数组中,而是接受一个数组并将其扩展为复合项。

遵循这一原则,扩展运算符有很多用例3。比如连接数组:

let knownFoesLevel1 = ['rat', 'rabbit']
let newFoes = ['globin', 'ghoul'];
let knownFoesLevel2 = [...knownFoesLevel1, ...newFoes];
Enter fullscreen mode Exit fullscreen mode

或者克隆它们:

let foes = ['globin', 'ghoul'];
let clonedFoes = [...foes];
Enter fullscreen mode Exit fullscreen mode

对象扩展和休息将数组中可用的相同类型的功能带到了对象中。

对象扩展运算符的一个很好的用例是混合宏 (mixin)。在之前的章节中,我们曾经Object.assign混合过两个或多个不同对象的属性。例如,在这个Wizard工厂函数中,我们将巫师属性与混合宏混合在一起,这些混合宏封装了通过名称识别某些对象并施放法术的行为:

function Wizard(element, mana, name, hp){
  let wizard = {element, 
                mana, 
                name, 
                hp};
  Object.assign(wizard, 
               canBeIdentifiedByName,
               canCastSpells);
  return wizard;
}
Enter fullscreen mode Exit fullscreen mode

我们可以使用对象扩展重写上面的示例,如下所示:

function Wizard(element, mana, name, hp){
  let wizard = {element, 
                mana, 
                name, 
                hp};

  // now we use object spread
  return {...wizard, 
          ...canBeIdentifiedByName,
          ...canCastSpells
         };
}
Enter fullscreen mode Exit fullscreen mode

对象扩展运算符的本质是:获取的所有属性,并将它们放在同一个对象 中wizardcanBeIdentifiedByNamecanCastSpells。如果存在任何同名的属性,则最后一个属性将覆盖第一个属性。

与对象展开相反的是对象剩余参数。它们的工作方式与 ES6 剩余参数类似,并且与 ES6 解构配合使用时尤其有用。

如果您还记得的话,我们使用解构和剩余参数从数组中提取元素:

let [first, second, ...rest] = ['dragon', 'chimera', 'harpy', 'medusa'];
console.log(first); // => dragon
console.log(second); // => chimera
console.log(rest); // => ['harpy', 'medusa']
Enter fullscreen mode Exit fullscreen mode

使用对象扩展运算符,我们可以按照相同的模式从对象中提取和收集属性:

let {name, type, ...stats} = {
  name: 'Hammer of the Morning',
  type: 'two-handed war hammer',
  weight: '40 pounds',
  material: 'nephirium',
  state: 'well kept'
};
console.log(name); // => Hammer of Morning
console.log(type); // => two-handed war hammer
console.log(stats); 
// => {weight: '40 pounds', 
//     material: 'nephirium', 
//     state: 'well kept'}
Enter fullscreen mode Exit fullscreen mode

还有更多!

TypeScript 中有很多功能可以在 ES6 上进行扩展,要么通过目前处于提案阶段的 ESnext 功能的早期实现(例如async/await或装饰器),要么通过全新的功能(例如我们已经看到的与类和枚举相关的功能)。

如果您有兴趣了解有关 TypeScript 的更多信息,那么我鼓励您查看TypeScript 手册和发行说明,它们都提供了有关 TypeScript 为您准备的内容的详细信息。

TypeScript 中的类型注释

类型注解是 TypeScript 的核心功能,它为 JavaScript 元编程带来了新的高度:类型元编程。类型注解能够通过更短的反馈循环、编译时错误和 API 可发现性,为您和您的团队创造更佳的开发体验。

TypeScript 中的类型注解不仅仅局限于简单的原始类型,例如stringnumber。你可以指定数组的类型:

// An array of strings
let saddleBag: string[] = [];
saddleBag.push('20 silvers');
saddleBag.push('pair of socks');

saddleBag.push(666);
// => [ts] Argument of type '666' is not assignable 
//         to parameter of type 'string'.
Enter fullscreen mode Exit fullscreen mode

和元组:

// A tuple of numbers
let position : [number, number];
position = [1, 1];
position = [2, 2];

// position = ['orange', 'delight'];
// => [ts] Type '[string, string]' is not 
//    assignable to type '[number, number]'.
//    Type 'string' is not assignable to type 'number'.
Enter fullscreen mode Exit fullscreen mode

功能:

// a predicate function that takes numbers and returns a boolean
let predicate: (...args: number[]) => boolean;
predicate = (a, b) => a > b
console.log(`1 greated than 2? ${predicate(1, 2)}`);
// => 1 greated than 2? false

predicate = (text:string) => text.toUpperCase();
// => [ts] Type '(text: string) => string' is not assignable 
//         to type '(...args: number[]) => boolean'.
//     Types of parameters 'text' and 'args' are incompatible.
//     Type 'number' is not assignable to type 'string'.
Enter fullscreen mode Exit fullscreen mode

甚至物体:

function frost(minion: {hitPoints: number}) {
  const damage = 10;
  console.log(`${minion} is covered in frozy icicles (- ${damage} hp)`);
  minion.hitPoints -= damage;
}
Enter fullscreen mode Exit fullscreen mode

表示{hitPoints: number}具有hitPoints类型属性的对象number。我们可以对危险的敌人施展冰霜魔法,但必须满足以下条件:拥有以下hitPoints属性:

const duck = {
  toString(){ return 'a duck';}, 
  hitPoints: 100
};

frost(duck);
// => a duck is covered in frozy icicles (-10hp)
Enter fullscreen mode Exit fullscreen mode

如果冻结的对象不满足要求,TypeScript 将立即提醒我们:

const theAir = {
    toString(){ return 'air';}
};
frost(theAir);
// => [ts] Argument of type '{ toString(): string; }' 
//    is not assignable to parameter 
//      of type '{ hitPoints: number; }'.
// Property 'hitPoints' is missing in type '{ toString(): string; }'.
Enter fullscreen mode Exit fullscreen mode

注释对象的更好方法是通过接口

TypeScript 接口

接口可重用,并且比直接使用对象类型注解更简洁。Minion接口可以描述如下:

interface Minion {
    hitPoints: number;
}
Enter fullscreen mode Exit fullscreen mode

我们可以使用这个新接口来更新我们的frost功能:

function frost(minion: Minion){
  const damage = 10;
  console.log(`${minion} is covered in frozy icicles (-${damage} hp)`);
  minion.hitPoints -= damage;
}
Enter fullscreen mode Exit fullscreen mode

看起来漂亮多了,不是吗?关于接口,一个有趣的事实是,它们完全是 TypeScript 的产物,其唯一应用范围是在类型注解和 TypeScript 编译器的范围内。正因为如此,接口不会被转译成 JavaScript。如果你转译上面的代码,你会惊讶地发现,生成的 JavaScript 代码中根本没有提到Minion

function frost(minion) {
    var damage = 10;
    console.log(minion + " is covered in frozy icicles (-" + damage + " hp)");
    minion.hitPoints -= damage;
}
Enter fullscreen mode Exit fullscreen mode

这表明,接口是一种向代码库添加类型注释的轻量级方法,可以在开发过程中获得好处,而不会对浏览器上运行的代码产生任何负面影响。

让我们用不同类型的参数测试一下新frost函数和Minion接口,看看它们的表现。继续duck之前的例子!

// const duck = {
//  toString(){ return 'duck';}, 
//  hitPoints: 100
//  };
frosty(duck);
// => duck is covered in frozy icicles (-10hp)
Enter fullscreen mode Exit fullscreen mode

这看起来完美无缺。如果我们尝试用一个表示 a 的类,Tower并且它包含 ahitPoints和 adefense属性,看起来也同样有效:

class Tower {
    constructor(public hitPoints=500, public defense=100){}
    toString(){ return 'a mighty tower';}
}
const tower = new Tower();

frosty(tower);
// => a mighty tower is covered in frozy icicles (-10hp)
Enter fullscreen mode Exit fullscreen mode

具有以下属性的简单对象文字也是如此hitPoints

frosty({hitPoints: 100});
// => [object Object] is covered in frozy icicles (-10hp)
Enter fullscreen mode Exit fullscreen mode

但是,如果我们使用除了hitPoints编译器之外还具有其他属性的对象文字,则会引发错误:

frosty({hitPoints: 120, toString(){ return 'a bat';}})
// => doesn't compile
// => Argument of type '{ hitPoints: number; toString(): string; }' 
//    is not assignable to parameter of type 'Minion'.
//  Object literal may only specify known properties, 
//  and 'toString' does not exist in type 'Minion'.
Enter fullscreen mode Exit fullscreen mode

错误信息似乎很有帮助。它说,对于对象字面量,我只能指定已知属性,而toString中不存在Minion。那么,如果我将对象字面量存储在变量 中会发生什么aBat

let aBat = {
    hitPoints: 120, 
    toString(){ return 'a bat';}
};
frosty(aBat);
// => a bat is covered in frozy icicles (-10hp)
Enter fullscreen mode Exit fullscreen mode

成功了!有趣的是,从这些实验来看,TypeScript 似乎会将 a 视为Minion任何满足接口所指定契约的对象,也就是说,具有hitPoints类型的属性number

然而,当你使用对象字面量时,TypeScript 似乎有一套更严格的规则,它要求参数与Minion接口完全匹配。那么,aMinion到底是什么呢?当 TypeScript 遇到一个任意对象时,它如何判断它是否是 aMinion呢?

它遵循结构类型的规则

结构化类型

结构类型是一种类型系统,其中类型兼容性和等效性由被比较类型的结构(即它们的属性)决定

例如,按照结构类型,以下所有类型都是等效的,因为它们具有相同的结构(相同的属性):

// an interface
interface Wizard {
  hitPoints: number;
  toString(): string;
  castSpell(spell:any, targets: any[]);
}

// an object literal
const bard = {
  hitPoints: 120,
  toString() { return 'a bard';},
  castSpell(spell: any, ...targets: any[]){
    console.log(`${this} cast ${spell} on ${targets}`);
    spell.cast(targets);
  }
}

// a class
class MagicCreature {
  constructor(public hitPoints: number){}
  toString(){ return "magic creature";}
  castSpell(spell: any, ...targets: any[]){
    console.log(`${this} cast ${spell} on ${targets}`);
    spell.cast(targets);
  }
}
Enter fullscreen mode Exit fullscreen mode

您可以使用以下代码片段进行验证:

let wizard: Wizard = bard;
let anotherWizard: Wizard = new MagicCreature(120);
Enter fullscreen mode Exit fullscreen mode

相比之下,像 C# 或 Java 这样的语言拥有所谓的名义类型系统。在名义类型系统中,类型等价性基于类型的名称和显式声明,其中 aMagicCreature是 a Wizard,当且仅当该类显式实现了接口。

结构化类型对于 JavaScript 开发者来说非常棒,因为它的行为非常类似于鸭子类型,而鸭子类型是 JavaScript 面向对象编程模型的核心特性。对于 C#/Java 开发者来说,它同样很棒,因为他们可以享受 C#/Java 的特性,例如接口、类和编译时反馈,但拥有更高的自由度和灵活性。

还有一个用例不符合我们刚才描述的结构类型规则。如果你还记得上一节中的例子,对象字面量似乎是结构类型规则的一个例外:

frosty({hitPoints: 120, toString(){ return 'a bat';}})
// => doesn't compile
// => Argument of type '{ hitPoints: number; toString(): string; }' 
//    is not assignable to parameter of type 'Minion'.
//  Object literal may only specify known properties, 
//  and 'toString' does not exist in type 'Minion'.
Enter fullscreen mode Exit fullscreen mode

为什么会发生这种情况?这样做是为了防止开发人员犯错。

TypeScript 编译器的设计者考虑到,像这样使用对象字面量很容易出错(比如拼写错误,想象一下hitPoitns把写成hitPoints)。因此,当以这种方式使用对象字面量时,TypeScript 编译器会格外谨慎,执行额外的属性检查。在这种特殊模式下,TypeScript 会格外小心,会标记出函数frosty不需要的任何额外属性。这一切都是为了帮助你避免不必要的错误。

如果您确定您的代码是正确的,您可以通过将对象文字明确转换为所需的类型或将其存储在变量中来快速告诉 TypeScript 编译器没有问题,就像我们之前看到的那样:

frosty({hitPoints: 120, toString(){ return 'a bat';}} as Minion);
// => a bat is covered in frozy icicles (-10hp)
Enter fullscreen mode Exit fullscreen mode

注意到 ? 了吗as Minion?这样我们就可以告诉 TypeScript 对象字面量的类型是Minion。这是另一种方法:

frosty((<Minion>{hitPoints: 120, toString(){ return 'a bat';}}));
// => a bat is covered in frozy icicles (-10hp)
Enter fullscreen mode Exit fullscreen mode

TypeScript 帮助你使用类型注释

TypeScript 另一个有趣的方面是它的类型推断功能。编写类型注解不仅会导致代码更加冗长,还会增加额外的工作量。为了最大限度地减少注释代码所需的工作量,TypeScript 会尽力从代码本身推断所使用的类型。例如:

const aNumber = 1;
const anotherNumber = 2 * aNumber;

// aNumber: number
// anotherNumber:number
Enter fullscreen mode Exit fullscreen mode

在此代码示例中,我们没有指定任何类型。无论如何,TypeScript 毫无疑问地知道该aNumber变量的类型是number,并且通过求值也anotherNumber知道它的类型是number。同样,我们可以编写以下内容:

const double = (n: number) => 2*n;
// double: (n:number) => number
Enter fullscreen mode Exit fullscreen mode

并且 TypeScript 会知道该函数double返回一个数字。

从接口到类

到目前为止,我们已经了解了如何以原始类型、数组、对象字面量和接口的形式使用类型注解。所有这些都是 TypeScript 特有的构件,当你将 TypeScript 代码转换为 JavaScript 时,它们就会消失。我们还了解了 TypeScript 如何尝试从你的代码中推断类型,这样你就无需花费不必要的时间来注解代码。

然后是类。类是 ES6/TypeScript 的一个特性,我们可以用它来描述领域模型实体的结构和行为,它包含具体的实现,同时也可以作为类型注解。

在前面的章节中,我们定义了一个接口Minion,它表示一个具有属性的事物hitPoints。我们可以对类做同样的事情:

class ClassyMinion {
  constructor(public hitPoints: number) {}
}
Enter fullscreen mode Exit fullscreen mode

并创建一个新classyFrost函数以使用此类作为参数类型:

function classyFrost(minion: ClassyMinion){
  const damage = 10;
  console.log(`${minion} is covered in frozy icicles (-${damage} hp)`)
  minion.hitPoints -= damage;
}
Enter fullscreen mode Exit fullscreen mode

我们可以将这个函数与我们的新ClassyMinion类一起使用,甚至可以与以前的aBatbard变量一起使用,因为遵循结构类型规则,所有这些类型都是等效的:

classyFrosty(new ClassyMinion());
// => a classy minion is covered in frozy icicles (-10hp)
classyFrosty(aBat);
// => a bat is covered in frozy icicles (-10hp)
classyFrosty(bard);
// => a bard is covered in frozy icicles (-10hp)
Enter fullscreen mode Exit fullscreen mode

通常我们会实现class所需的interface。例如:

class ClassyMinion implements Minion {
  constructor(public hitPoints: number) {}
}
Enter fullscreen mode Exit fullscreen mode

从结构类型的角度来看,这不会改变这个类的外观,但确实提升了我们的开发者体验。添加这些属性implements Minion可以帮助 TypeScript 判断我们是否正确实现了接口,或者是否缺少任何属性或方法。对于只有一个属性的类来说,这听起来可能没什么用,但随着类变得越来越丰富,它的作用会越来越大。

一般来说,使用 aclass和使用 an之间的区别interface在于,当转换为 JavaScript 时,该类将产生一个真正的 JavaScript 类(尽管它可能是一个构造函数/原型对,具体取决于您所针对的 JavaScript 版本)。

例如,上面的类将在我们当前的设置中产生以下 JavaScript:

var ClassyMinion = (function () {
    function ClassyMinion(hitPoints) {
        if (hitPoints === void 0) { hitPoints = 100; }
        this.hitPoints = hitPoints;
    }
    ClassyMinion.prototype.toString = function () {
        return 'a classy minion';
    };
    return ClassyMinion;
}());
Enter fullscreen mode Exit fullscreen mode

这是有道理的,因为与interface仅在 TypeScript 类型注释世界中使用的虚构工件不同,aclass是运行程序所必需的。

那么什么时候使用接口,什么时候使用类呢?让我们回顾一下这两种结构的作用以及它们的行为方式:

  • 接口:描述形状和行为。它在编译过程中会被移除。
  • :描述形状和行为,提供具体的实现。它被编译成 JavaScript。

所以接口和类都描述了类型的形状和行为。此外,类还提供了具体的实现。

在 C# 或 Java 的世界里,遵循依赖倒置原则,我们建议在描述类型时优先使用接口而不是类。这将为我们的程序带来极大的灵活性和可扩展性,因为我们将实现一个松耦合的系统,其中具体类型彼此之间互不了解。这样,我们就可以注入各种具体类型,以满足接口定义的契约。这在 C# 或 Java 等静态类型语言中是必须的,因为它们使用名义类型系统。但是 TypeScript 呢?

正如我们之前提到的,TypeScript 使用结构化类型系统,当类型具有相同的结构(即相同的成员)时,它们是等效的。鉴于此,你可以说使用接口还是类来表示类型并不重要。如果接口、类或对象字面量共享相同的结构,它们就会被平等对待,那么为什么我们需要在 TypeScript 中使用接口呢?以下是考虑使用接口还是类时可以遵循的一些准则:

  1. 单一职责是降低程序复杂性的一条重要经验法则。将单一职责应用于接口与类的困境,我们可以得出使用接口表示类型、使用类表示实现的结论。接口提供了一种非常简洁的方式来表示类型的结构,而类则将结构和实现混杂在一起,这使得仅通过查看类很难确定类型的结构。
  2. interfaces比类更灵活。由于类包含特定的实现,因此本质上比接口更严格。使用接口,我们可以捕获类之间共有的细粒度细节或行为片段。
  3. interfaces是一种轻量级的方法,可以为应用程序不熟悉的数据(例如来自 Web 服务的数据)提供类型信息。
  4. 对于没有附加行为的类型,例如仅仅是数据的类型,可以直接使用类。在这种情况下,使用接口通常是多余的,没有必要的。使用类可以简化通过构造函数创建对象的过程。

因此,一般来说,我们在 C# 和 Java 等静态类型语言中遵循的接口准则也适用于 TypeScript。建议使用接口来描述类型,并使用类来实现特定的实现。如果类型只是数据,没有行为,则可以考虑使用单独的类。

高级类型注解

除了我们目前所见之外,TypeScript 还提供了更多机制来在程序中表达更复杂的类型。其理念是,无论您使用哪种 JavaScript 构造或模式,都应该能够通过类型注解来表达其类型,并为您和团队中的其他开发人员提供有用的类型信息。

这些高级类型注释的一些示例是:

  • 泛型
  • 交集和并集类型
  • 类型保护
  • 可空类型
  • 类型别名
  • 字符串文字类型

让我们看一下它们中的每一个,为什么需要它们以及如何使用它们。

泛型

泛型是 C# 和 Java 等静态类型编程语言中常用的一种技术,用于将数据结构或算法的应用推广到多种类型。

例如,不必为每种不同类型单独实现ArrayNumberArray,,,等:StringArrayObjectArray

interface NumberArray {
  push(n: number);
  pop(): number;
  [index: number]: number;
  // etc
}

interface StringArray {
  push(s: string);
  pop(): string;
  [index: number]: string;
  // etc
}

// etc...
Enter fullscreen mode Exit fullscreen mode

我们使用泛型来描述Array任意类型T

// note that `Array<T>` is already a built-in type in TypeScript
interface Array<T>{
  push(s: T);
  pop(): T;
  [index: number]: T;
  // etc
}
Enter fullscreen mode Exit fullscreen mode

我们现在可以通过选择类型来重用这个单一类型定义T

let numbers: Array<number>;
let characters: Array<string>;
// and so on...
Enter fullscreen mode Exit fullscreen mode

就像我们在接口中使用泛型一样,我们可以在类中使用泛型:

class Cell<T> {
  private prisoner: T;

  inprison(prisoner: T) { 
    this.prisoner = item; 
  }

  free(): T { 
    const prisoner = this.prisoner; 
    this.prisoner = undefined;
    return prisoner;
  }
}
Enter fullscreen mode Exit fullscreen mode

最后,你可以将类型限制T为仅包含特定类型的子集。例如,假设某个函数仅在 的上下文中有意义Minion。你可以这样写:

interface ConstrainedCell<T extends Minion>{
  inprison(prisoner: T);
  free(): T;
}
Enter fullscreen mode Exit fullscreen mode

现在这将是一个完全可用的盒子:

let box: ConstrainedCell<MagicCreature>;
Enter fullscreen mode Exit fullscreen mode

但这不会因为类型与接口T不匹配Minion

let box: ConstrainedCell<{name: string}>;
// => [ts] Type '{ name: string; }' 
//    does not satisfy the constraint 'Minion'.
//    Property 'hitPoints' is missing in type '{ name: string; }'.
Enter fullscreen mode Exit fullscreen mode

交集和并集类型

我们已经看到了原始类型、接口、类、泛型,以及提供类型信息的许多不同方法,但尽管这些方法可能很灵活,但仍然有一个用例它们很难涵盖:Mixins

使用 mixin 时,生成的对象是其他不同对象的混合。该对象本身的类型并非已知类型,而是现有类型的组合。

例如,让我们回到之前的向导示例:

function Wizard(element, mana, name, hp){
  let wizard = {element, 
                mana, 
                name, 
                hp};

  // now we use object spread
  return {...wizard, 
          ...canBeIdentifiedByName,
          ...canCastSpells
         };
}
Enter fullscreen mode Exit fullscreen mode

我们可以将其分解为单独的元素:

interface WizardProps{
  element: string;
  mana: number;
  name: string;
  hp: number;
}

interface NameMixin {
  toString(): string;
}

interface SpellMixin {
  castsSpell(spell:Spell, target: Minion);
}
Enter fullscreen mode Exit fullscreen mode

我们如何定义由Wizard组合而成的结果类型呢?我们使用交叉类型。交叉类型允许我们定义由其他类型组合而成的类型。例如,我们可以使用以下类型注解来表示:WizardPropsNameMixinSpellMixinWizard

WizardProps & NameMixin & SpellMixin
Enter fullscreen mode Exit fullscreen mode

我们可以将其用作工厂函数的返回类型:

let canBeIdentifiedByName: NameMixin = {
  toString(){ return this.name; }
};

let canCastSpells: SpellMixin = {
  castsSpell(spell:Spell, target:Minion){
    // cast spell
  }
}

function WizardIntersection(element: string, mana: number, 
                            name : string, hp: number): 
         WizardProps & NameMixin & SpellMixin {
  let wizard: WizardProps = {element, 
                mana, 
                name, 
                hp};

  // now we use object spread
  return {...wizard, 
          ...canBeIdentifiedByNameMixin,
          ...canCastSpellsMixin
         };
}

const merlin = WizardIntersection('spirit', 200, 'Merlin', 200);
// merlin.steal(conan);
// => [ts] Property 'steal' does not exist 
//    on type 'WizardProps & NameMixin & SpellMixin'.
Enter fullscreen mode Exit fullscreen mode

正如我们拥有交集类型(其结果是其他类型的组合)一样,我们也能够创建一个可以是一系列类型中的任意一个的类型,即“要么是”或“是”stringnumber其他类型。我们将这些类型称为“联合类型”。它们通常用于重载函数或方法,这些函数或方法可能需要不同类型的参数。

看一下下面这个组建骷髅军队的函数:

function raiseSkeleton(numberOrCreature){
  if (typeof numberOrCreature === "number"){
    raiseSkeletonsInNumber(numberOrCreature);
  } else if (typeof numberOrCreature === "string") {
    raiseSkeletonCreature(numberOrCreature);
  } else {
    console.log('raise a skeleton');
  }

  function raiseSkeletonsInNumber(n){
    console.log('raise ' + n + ' skeletons');
  }
  function raiseSkeletonCreature(creature){
    console.log('raise a skeleton ' + creature);
  };
}
Enter fullscreen mode Exit fullscreen mode

numberOrCreature根据上述功能的类型可以培养骷髅或骷髅生物:

raiseSkeleton(22);
// => raise 22 skeletons

raiseSkeleton('dragon');
// => raise a skeleton dragon
Enter fullscreen mode Exit fullscreen mode

raiseSkeletonTS我们可以使用联合类型为函数添加一些 TypeScript 优点:

function raiseSkeletonTS(numberOrCreature: number | string){
  if (typeof numberOrCreature === "number"){
    raiseSkeletonsInNumber(numberOrCreature);
  } else if (typeof numberOrCreature === "string") {
    raiseSkeletonCreature(numberOrCreature);
  } else {
    console.log('raise a skeleton');
  }

  function raiseSkeletonsInNumber(n: number){
    console.log('raise ' + n + ' skeletons');
  }
  function raiseSkeletonCreature(creature: string){
    console.log('raise a skeleton ' + creature);
  };
}
Enter fullscreen mode Exit fullscreen mode

number | string一个联合类型,允许numberOrCreaturenumber或类型string。如果我们错误地使用了其他类型,TypeScript 会为我们提供支持:

raiseSkeletonTS(['kowabunga'])
// => [ts] Argument of type 'string[]' is not assignable 
//         to parameter of type 'string | number'.
// Type 'string[]' is not assignable to type 'number'.
Enter fullscreen mode Exit fullscreen mode

类型保护

联合类型在函数体中会引发特殊情况。如果联合类型numberOrCreature可以是数字或字符串,TypeScript 如何知道哪些方法受支持?数字方法与字符串方法差别很大,那么允许哪些方法呢?

当 TypeScript 遇到像上面函数中那样的联合类型时,默认情况下,你只能使用所有包含的类型中可用的方法和属性。只有当你进行显式转换或包含类型保护时,TypeScript 才能确定所使用的类型并为你提供帮助。幸运的是,TypeScript 可以识别常见的 JavaScript 类型保护模式,例如typeof我们在上一个示例中使用的 。执行类型保护后,if (typeof numberOrCreature === "number")TypeScript 可以确定你在 if 块中执行的任何代码段都numberOrCreature将是 类型number

类型别名

另一个与交集类型和并集类型完美兼容的机制是类型别名。类型别名允许您提供任意名称(别名)来引用其他类型。厌倦了编写这种交集类型了吗?

WizardProps & NameMixin & SpellMixin
Enter fullscreen mode Exit fullscreen mode

您可以创建一个别名Wizard并使用它来代替:

type Wizard = WizardProps & NameMixin & SpellMixin;
Enter fullscreen mode Exit fullscreen mode

此别名将允许您根据前面的示例改进向导工厂:

function WizardAlias(element: string, mana: number, 
                name : string, hp: number): Wizard {
  let wizard: WizardProps = {element, 
                mana, 
                name, 
                hp};

  // now we use object spread
  return {...wizard, 
          ...canBeIdentifiedByNameMixin,
          ...canCastSpellsMixin
         };
}
Enter fullscreen mode Exit fullscreen mode

更多类型注释!

尽管我试图在本书的最后一章中全面介绍 TypeScript,但还有很多功能和有趣的东西,除非我写一本关于 TypeScript 的完整书籍,否则我将无法介绍它们。

如果您有兴趣了解有关使用 TypeScript 类型注释可以做的所有有趣的事情,那么请让我在TypeScript 手册发行说明中再次强调。

在实际应用程序中使用 TypeScript

所以 TypeScript 很棒,它在 ES6 的基础上为您提供了许多出色的新功能,并通过类型注释提供了出色的开发人员体验,但是如何在现实世界的应用程序中开始使用它呢?

好消息是,您几乎不需要从头开始创建 TypeScript 设置。最流行的前端框架都内置了对 TypeScript 的支持。例如,TypeScript 是 Angular 的主要语言,使用 Angular 和 TypeScript 创建一个新项目就像使用 Angular cli 并输入以下内容一样简单:

$ ng new my-new-app
Enter fullscreen mode Exit fullscreen mode

同样,使用 React 和Create React App工具(也称为 CRA),使用 TypeScript 启动 React 项目只需输入4行:

$ create-react-app my-new-app --scripts-version=react-scripts-ts
Enter fullscreen mode Exit fullscreen mode

如果您使用以上任何选项,就可以开始了。无论哪种情况,系统都会为您引导一个新应用,然后您就可以开始使用 TypeScript 构建 Angular 或 React 应用了。

另一方面,如果你出于某种原因需要从头开始,你会发现最常见的任务管理器或模块打包器(例如 grunt、gulp 或 webpack)都有TypeScript 插件。在将 TypeScript 集成到你的工具链中时,你可能需要执行一个额外的步骤来配置 TypeScript 编译器:设置你的tsconfig文件。

文件tsconfig.json

tsconfig.json文件包含项目的 TypeScript 配置。它告诉 TypeScript 编译器编译项目所需的所有细节,例如:

  • 需要转换哪些文件
  • 要忽略哪些文件
  • 使用哪个版本的 JavaScript 作为转译的目标
  • 在输出 JavaScript 中使用哪个模块系统
  • 编译器应该有多严格?它应该允许隐式 any 吗?应该执行严格的空值检查吗?
  • 加载哪些第三方库类型

如果您未指定部分信息,TypeScript 编译器将尽力而为。例如,如果您未指定任何要转译的文件,TypeScript 编译器将转译*.ts项目文件夹中的所有 TypeScript 文件( )。如果您未指定任何第三方类型,TypeScript 编译器将在您的项目中查找类型定义文件( 中的 fi ./node_modules/@types)。

这是tsconfig.jsonTypeScript 文档中的一个示例,可以给你一个想法:

{
    "compilerOptions": {
        "module": "system",
        "noImplicitAny": true,
        "removeComments": true,
        "preserveConstEnums": true,
        "outFile": "../../built/local/tsc.js",
        "sourceMap": true
    },
    "include": [
        "src/**/*"
    ],
    "exclude": [
        "node_modules",
        "**/*.spec.ts"
    ]
}
Enter fullscreen mode Exit fullscreen mode

有关所有可用选项的完整参考,请查看TypeScript 文档

在本章的示例中,我们没有使用 tsconfig。为什么?

TypeScript 编译器tsc有两种不同的运行模式:有或无输入文件。
如果在执行过程中未指定输入文件, TypeScript 编译器将尝试通过其配置tsc查找可用的文件。 如果指定了输入文件,TypeScript 编译器将忽略。这就是为什么在前面几节中,我们在运行 时无需定义文件tsconfig.json
tsconfig.jsontsconfig.jsontsc hello-wizard.ts

TypeScript 和第三方库

从 TypeScript 2.0 开始,安装第三方库的类型声明与通过 安装任何其他库一样简单npm

想象一下,您想要利用ramda.js这个库,它具有实用的实用函数和强大的函数式编程风格,我们将在 JavaScript-mancy 的函数式编程巨著中深入了解。

您可以使用 npm 将库添加到您的 TypeScript 项目中:

# create package.json
$ npm init 

# install ramda and save dependency
$ npm install --save ramda
Enter fullscreen mode Exit fullscreen mode

您可以使用以下命令安装该库的类型声明@types/<name-of-library-in-npm>

$ npm install --save-dev @types/ramda
Enter fullscreen mode Exit fullscreen mode

现在,当您在 Visual Studio Code 或您选择的编辑器中开始开发项目时,使用 ramda.js 应该可以获得完整的类型支持。尝试编写下面的代码片段,并验证 TypeScript 如何帮助您:

import { add } from 'ramda';

const add5 = add(5);

console.log(`5 + 5: ${add5(5)}`);
console.log(`5 + 10: ${add5(1)}`);
Enter fullscreen mode Exit fullscreen mode

所有这些类型定义都来自DefinitelyTyped项目,并按照惯例定期以 前缀推送到 npm @types/。如果您找不到某个库的类型声明,可以使用TypeSearch Web 应用进行查找(stampit例如,您可以尝试从“邮票”章节中查找)。

结论

这就是 TypeScript!这是本书最长的一章,但我希望它足够有趣,能够让你坚持到最后。让我们快速回顾一下,帮助你快速记住刚刚学到的 TypeScript 的所有精彩内容。

TypeScript 是 JavaScript 的超集,包含许多 ESnext 特性和类型注解。目前为止,TypeScript 最显著的特征是其对类型的使用。类型注解允许你提供有关代码的额外元数据,TypeScript 编译器可以使用这些元数据,从而以牺牲代码的冗长性为代价,为您和您的团队提供更好的开发体验。

TypeScript 是 ES6 的超集,它扩展了 ES6 的功能,包含大量 ESnext 改进和 TypeScript 特有的功能。我们了解了 ESnext 的几个特性,例如类成员以及新的 Objects 扩展运算符和 Rest 运算符。我们还了解了 TypeScript 如何通过参数属性和属性访问器增强类,并引入了新的 Enum 类型,让您能够编写更具针对性的代码。

类型注解是 TypeScript 的核心。TypeScript 扩展了 JavaScript,赋予其新的语法和语义,让您能够提供关于应用程序类型的丰富信息。除了能够表达原始类型之外,TypeScript 还引入了接口、泛型、交集和联合类型、别名、类型保护等等……所有这些机制都允许您进行一种新型的元编程,从而通过类型注解提升您的开发体验。然而,添加类型注解可能有点令人望而生畏,需要大量的工作,为了最大限度地减少这种情况,TypeScript 会尝试从您的代码中推断出尽可能多的类型。

秉承 JavaScript 和鸭子类型的精神,TypeScript 拥有结构化类型系统。这意味着,如果类型具有相同的结构(即具有相同的属性),则它们是等价的。这与 C# 或 Java 中使用的名义类型系统相反,在这些系统中,类型等价性是通过显式实现类型来确定的。结构化类型的优点在于它提供了极大的灵活性,同时还提供了出色的编译时错误检测和改进的工具。

在前端开发领域,我们看到 TypeScript 的采用率正在不断提高,尤其是在它成为 Angular 开发的核​​心语言之后。此外,它也适用于大多数常见的前端框架、IDE、文本编辑器和前端构建工具。它也通过类型定义和 DefinitelyTyped 项目在第三方库中得到了良好的支持,并且为库安装类型定义就像执行npm install.

从个人角度来看,我从 C# 的世界转移到 JavaScript 最喜欢它的一点是它的简洁、没有繁琐的程序和不必要的构件。突然之间,我不再需要编写代码PurchaseOrder purchaseOrderEmployee employee员工就是一个employee…… 。我不需要编写看似无限的样板代码来让我的应用程序灵活且可扩展,也不需要与语言斗争以使其屈从于我的意愿,一切就这么简单。当我看到 TypeScript 的发布时,我担心 JavaScript 会失去灵魂,变得像 C# 或 Java 一样死板。在体验了使用 TypeScript 开发 Angular 应用程序、它的可选类型、出色的开发体验,以及最重要的,它具有结构化类型之后,我充满了希望。关注未来几个月和几年的发展将会很有趣。很可能我们所有人最终都会以编写 TypeScript 为生。

mooleen.says('You shall only use types!?...');

bandalf.says("I've got my magic back... " + 
  "but for some reason it won't... work");

mooleen.says("I, too, can feel the bond with the " +
  "currents of magic again");

randalf.says("The Order of the Red Moon...");

red.says("There are our weapons! Under the obelisk!");

/*
The group makes a precarious circle beside the obelisk as
the hordes of lizard-like beast surround them.
*/

randalf.says("types... Yes! " + 
  "Now I remember, The Last Stand and the Sacred Order. " +
  "Their story lies between history and legend. " +
  "It is said that they cultivated an obscure " +
  "flavor of JavaScriptmancy. The legends say that " +
  "they expanded it and enriched it with types...");

bandalf.says("Excellent. And what does that mean?");

rat.says("It means we're dead");
red.says("A glorious death!");

randalf.says("Well they were a very guarded Order " +
  "and they were exterminated to the last woman " + 
  "in The Last Stand or so the story says..." + 
  "In the deep jungles of Azons.");

mooleen.whispers("Azons...");

/* 
  The sisters surround her on the battlements,
  all wearing the black of the order in full armor.
  The fort has an excellent view of the thick,
  beautiful jungle below and of the unending hosts
  of lizardmen surrounding them.
  The Grand Commander shouts: 'To Arms sisters!'
  'For one last time!'
*/

mooleen.says("Types... Types... Types!");
mooleen.says("I remember...");

练习

实验 JavaScriptmancer!

您可以从GitHub下载源代码来试验这些练习和一些可能的解决方案

赢取时间!冰墙!

野兽们正在快速逼近,为了获得喘息的空间,在它们和队伍之间筑起一道冰墙。冰墙至少要100高、7深、700长各一英尺,才能将队伍包围。

墙应该满足以下代码片段:

const iceWall = new Wall(MagicElement.Ice, {
                          height: 100,
                          depth: 7, 
                          length: 700});

console.log(iceWall.toString());
// => A wall of frozen ice. It appears to be about 100 feet high
//    and extends for what looks like 700 feet.

iceWall.element = MagicElement.Fire;
// => [ts] Cannot assign to 'element' because it is 
//         a constant or a read-only property.
iceWall.wallOptions.height = 100;
// => [ts] Cannot assign to 'height' because it is 
//         a constant or a read-only property.

提示:您可以使用枚举来表示MagicElement,使用接口来表示 ,WallSpecifications以及使用类来表示Wall本身。记得添加类型注释!

解决方案


enum MagicElement {
  Fire = "fire",
  Water = "water",
  Earth = "earth",
  Air = "windy air",
  Stone = "hard stone",
  Ice = "frozen ice"
}

interface WallSpecs{
  readonly height: number,
  readonly depth: number,
  readonly length: number
}

class Wall {
  constructor(readonly element: MagicElement, 
              readonly specs: WallSpecs){ }

  toString(){
    return `A wall of ${this.element}. It appears to be about ` +
      `${this.specs.height} feet high and extends for what ` +
      `looks like ${this.specs.length} feet.`;
  }
}

const iceWall = new Wall(MagicElement.Ice, {
                          height: 100,
                          depth: 7, 
                          length: 700});

console.log(iceWall.toString());
// => A wall of frozen ice. It appears to be about 100 feet high
//    and extends for what looks like 700 feet long.

// iceWall.element = MagicElement.Fire;
// => [ts] Cannot assign to 'element' because it is 
//         a constant or a read-only property.
// iceWall.wallOptions.height = 100;
// => [ts] Cannot assign to 'height' because it is 
//         a constant or a read-only property.

world.randalf.gapes()
// => Randalf gapes

world.randalf.says('How?');
world.mooleen.says('I just remembered...');

world.randalf.says('Remember?');
world.randalf.says("You look very young for being millennia old");

world.mooleen.shrugs();
// => Moleen shrugs
world.mooleen.says("Brace yourselves... they're coming " + 
  "beware if they open their jaws and seem to catch breath " +
  "they breathe fire");

冻住蜥蜴!

你赢得了一些时间。现在你可以稍事休息,观察蜥蜴,为它们制作合适的模型,并制造一个frost咒语,将它们送入蜥蜴冰冻地狱。

您可以观察到以下情况:

giantLizard.jumps();
// => The giant lizard gathers strength in its 
//    4 limbs and takes a leap through the air
giantLizard.attacks(red);
// => The giant lizard attacks Red with great fury
giantLizard.breathesFire(red);
// => The giant lizard opens his jaws unnaturally wide
//    takes a breath and breathes a torrent of flames
//    towards Red
giantLizard.takeDamage(Damage.Physical, 20);
// => The giant lizard has extremely hard scales
//    that protect it from physical attacks (Damage 50%)
//    You damage the giant lizard (-10hp)
giantLizard.takeDamage(Damage.Cold, 20);
// => The giant lizard is very sensitive to cold.
//    It wails and screams. (Damage 200%)
//    You damage the giant lizard (-40hp)

创建一个frost满足此代码片段的咒语:

frost(giantLizard, /* mana */ 10);
// => The air surrounding the target starts quickly forming a
//    frozen halo as the water particles start congealing.
//    All of the sudden it explodes into freezing ice crystals
//    around the giant lizard.
//    The giant lizard is very sensitive to cold.
//    It wails and screams. (Damage 200%)
//    You damage the giant lizard (-2000hp)

提示:根据上述观察创建一个接口,并在你的frost函数中使用该新类型。思考一下对敌人造成伤害所需的契约。

解决方案


enum DamageType {
  Physical,
  Ice,
  Fire,
  Poison
}

// We only need an interface that
// describes something that can be damaged
interface Damageable{
  takeDamage(damageType: DamageType, damage: number);
}

function frost(target: Damageable, mana: number){
  // from the example looks like damage 
  // can be calculated based on mana
  const damage = mana * 100;
  console.log(
    `The air surrounding the target starts quickly forming a ` + 
    `frozen halo as the water particles start congealing. ` +
    `All of the sudden it explodes into freezing ice crystals ` +
    `around the ${target.toString()}.`);
  target.takeDamage(DamageType.Ice, damage);
}

console.log('A giant lizard leaps inside the wall!');
// this method returns a Lizard object (see samples)
const giantLizard = world.getLizard();

world.mooleen.says('And that is as far as you go');

frost(giantLizard, /* mana */ 2);
// => The air surrounding the target starts quickly forming a 
//    frozen halo as the water particles start congealing. 
//    All of the sudden it explodes into freezing ice crystals 
//    around the giant lizard.
//    The giant lizard is very sensitive to cold.
//    It wails and screams. (Damage 200%)
//    You damage the giant lizard (-400hp)
//    The giant lizard dies.

world.mooleen.laughsWithGlee();
// => Mooleen laughs with Glee

/* 
More and more lizards make it into the fortified area.
Mooleen, Red, randalf and bandalf form a semicircle against
the obsidian obelisk and fight fiercely for every inch.
When the lizards are about to overwhelm the group a huge furry
figure flashes in front of them charging through the lizard 
front line and causing enough damage to let the company regroup.
*/

world.mooleen.says('What?');
world.rat.says('Happy to serve!');

world.mooleen.says('You can do that?!');
world.rat.says('Err... we familiars are very flexible creatures');

world.mooleen.says("Why didn't you say it before?");
world.rat.says("Oh... the transformation is incredibly painful");
world.rat.says("And I bet you'd want to ride on my back." + 
    "I'm not putting up with that");

大规模破坏!

逐个击杀这些野兽根本行不通。我们需要一个更强大的法术,能够成群结队地消灭它们。设计一个iceCone可以同时作用于多个目标的法术。

它应该满足以下代码片段:

iceCone(lizard, smallerLizard, greaterLizard);
// => Cold ice crystals explode from the palm of your hand
//    and impact the lizard, smallerLizard, greaterLizard.
//    The lizard is very sensitive to cold.
//    It wails and screams. (Damage 200%)
//    You damage the giant lizard (-500hp)
//    The smaller lizard is very sensitive to cold.
//    It wails and screams. (Damage 200%)
//    You damage the giant lizard (-500hp)
//    etc...

提示:您可以使用剩余参数和数组类型注释!

解决方案

function iceCone(...targets: Damageable[]){
  const damage = 500;
  console.log(`
Cold ice crystals explode from the palm of your hand
and impact the ${targets.join(', ')}.`);
  for(let target of targets) {
    target.takeDamage(DamageType.Ice, damage);
  }
}

iceCone(getLizard(), getLizard(), getLizard());
// => Cold ice crystals explode from the palm of your hand
// and impact the giant lizard, giant lizard, giant lizard.
// The giant lizard is very sensitive to cold.
// It wails and screams. (Damage 200%)
// You damage the giant lizard (-1000hp)
// The giant lizard dies.
// etc...

world.mooleen.says('Yes!');

/* 
Mooleen looks around. She's fending off the lizards fine but
her companions are having some problems. 

Red is deadly with the lance and shield but his lance, 
in spite of of his massive strength, hardly penetrates
the lizards' thick skin. 

Bandalf is slowly catching up and crafting ice spells 
and Randalf, though, a master with the quarterstaff can
barely fend off the attacks from a extremely huge lizard.

Things start to look grimmer and grimmer as more lizards jump over
the wall around the obelisk.
*/

world.mooleen.says('I need to do something quick');

用魔法赋予你的同伴力量!

形势不容乐观。你唯一的机会就是强化你的同伴,组成强大的联合阵线,对抗日益强大的敌人。打造一个enchant能够为武器和盔甲赋予元素属性的法术。

enchant咒语应满足以下代码片段:

quarterstaff.stats();
// => Name: Crimson Quarterstaff
// => Damage Type: Physical
// => Damage: d20
// => Bonus: +20
// => Description: A quarterstaff of pure red

enchant(quarterstaff, MagicElement.Ice);
// => You enchant the Crimson Quarterstaff with a frozen 
//    ice incantation
//    The weapon gains Ice damage and +20 bonus damage

quarterstaff.stats();
// => Name: Crimson Quarterstaff
// => Damage Type: Ice
// => Damage: d20
// => Bonus: +40

cloak.stats();
// => Name: Crimson Cloak
// => Type: cloak
// => Protection: 20
// => ElementalProtection: none
// => Description: A cloak of pure red

enchant(cloak, MagicElement.Fire);
// => You enchant the Crimson Cloak with a fire incantation 
//    The Crimson Cloak gains +20 fire protection

cloak.stats();
// => Name: Crimson Cloak
// => Type: cloak
// => Protection: 20
// => ElementalProtection: Fire (+20)
// => Description: A cloak of pure red

提示:在咒语中使用联合类型和类型保护,enchant以允许它同时附WeaponArmor

解决方案


class Weapon {
  constructor(public name: string,
              public damageType: DamageType,
              public damage: number,
              public bonusDamage: number,
              public description: string){}
  stats(){
    return `
Name: ${this.name}
Damage Type: ${this.damageType}
Damage: d${this.damage}
Bonus: +${this.bonusDamage}
Description: ${this.description}
      `;
  }

  toString() { return this.name; }
}

enum ArmorType {
  Cloak = 'cloak',
  Platemail = 'plate mail'
}

interface ElementalProtection {
  damageType: DamageType;
  protection: number;
}

class Armor {
  elementalProtection: ElementalProtection[] = [];
  constructor(public name: string,
              public type: ArmorType,
              public protection: number,
              public description: string){}
  stats(){
    return `
Name: ${this.name}
Type: ${this.type}
Protection: ${this.protection}
ElementalProtection: ${this.elementalProtection.join(', ') || 'none'}
Description: ${this.description}
      `;
  }
  toString() { return this.name; }
}

function enchant(item: Weapon | Armor, element: MagicElement){
  console.log(`You enchant the ${item} with a ${element} incantation`);
  if (item instanceof Weapon){
    enchantWeapon(item, element);
  } else{
    enchantArmor(item, element);
  }

  function enchantWeapon(weapon: Weapon, element: MagicElement){
    const bonusDamage = 20;
    weapon.damageType = mapMagicElementToDamage(element);
    weapon.bonusDamage += bonusDamage;
    console.log(`The ${item} gains ${bonusDamage} ` + 
                `${weapon.damageType} damage`);
  }
  function enchantArmor(armor: Armor, element: MagicElement){
    const elementalProtection = {
      damageType: mapMagicElementToDamage(element),
      protection: 20,
      toString(){ return `${this.damageType} (+${this.protection})`}
    };
    armor.elementalProtection.push(elementalProtection);
    console.log(`the ${item} gains ${elementalProtection.protection}` + 
                ` ${elementalProtection.damageType} incantation`);
  }
}

function mapMagicElementToDamage(element: MagicElement){
  switch(element){
    case MagicElement.Ice: return DamageType.Ice;
    case MagicElement.Fire: return DamageType.Fire;
    default: return DamageType.Physical;
  }
}

let quarterstaff = getQuarterstaff();
console.log(quarterstaff.stats());
// => Name: Crimson Quarterstaff
//    Damage Type: Physical
//    Damage: d20
//    Bonus: +20
//    Description: A quarterstaff of pure red

enchant(quarterstaff, MagicElement.Ice);
// => You enchant the Crimson Quarterstaff with a frozen ice incantation
//    The Crimson Quarterstaff gains 20 Ice damage

console.log(quarterstaff.stats());
// Name: Crimson Quarterstaff
// Damage Type: Ice
// Damage: d20
// Bonus: +40
// Description: A quarterstaff of pure red

let cloak = getCloak();
console.log(cloak.stats());
// Name: Crimson Cloak
// Type: cloak
// Protection: 20
// ElementalProtection: none
// Description: A cloak of pure red

enchant(cloak, MagicElement.Fire);
// You enchant the Crimson Cloak with a fire incantation
// the Crimson Cloak gains 20 Fire incantation

console.log(cloak.stats());
// Name: Crimson Cloak
// Type: cloak
// Protection: 20
// ElementalProtection: Fire (+20)
// Description: A cloak of pure red

world.mooleen.says('Awesome! This will do!');

/*

As soon as Mooleen enchants the group's weapons and
armor the battle takes a different turn. Where previously
a lizard would've remained impassible after receiving a wound
now there's wails and shouts of beast pain surrounding 
the group...

*/

world.mooleen.says('haha! To Arms Sisters!');
world.red.says('What?');

  1. 你使用的编辑器应该与 TypeScript 编译器良好集成,才能提供此类服务。许多常见的 IDE 和文本编辑器都支持此功能 。↩

  2. params就像在 C# 中 一样。↩

  3. 回去复习一下 JavaScript-mancy:入门指南,了解更多用例! 

  4. 此命令使用 TypeScript React 在后台启动http://bit.ly/ts-react-starter  

文章来源:https://dev.to/vintharas/typescript-javascript--types--awesome-developer-productivity-1048
PREV
TypeScript 类型深入探究 - 第 3 部分:函数
NEXT
关于大型科技公司面试的思考