TypeScript,减去 TypeScript

Avatar of Caleb Williams
Caleb Williams

DigitalOcean 为您旅程的每个阶段提供云产品。立即开始使用 200 美元的免费额度!

除非您过去几年一直躲在石头底下(老实说,有时躲在石头底下感觉像是正确的事情),否则您可能听说过并且可能使用过 TypeScript。TypeScript 是 JavaScript 的语法超集,它添加了——顾名思义——类型到网络最喜欢的脚本语言。

TypeScript 非常强大,但对于初学者来说通常难以阅读,并且由于额外的无效 JavaScript 语法,需要编译步骤才能在浏览器中运行,从而增加了开销。对于许多项目来说,这不是问题,但对于其他项目来说,这可能会妨碍工作的完成。幸运的是,TypeScript 团队已经启用了一种使用 JSDoc 对普通 JavaScript 进行类型检查的方法。

设置新项目

要在新项目中启动并运行 TypeScript,您需要 NodeJS 和 npm。让我们从创建一个新项目并运行 npm init 开始。出于本文的目的,我们将使用 VShttps://vscode.js.cnCode 作为我们的代码编辑器。设置完成后,我们需要安装 TypeScript

npm i -D typescript

安装完成后,我们需要告诉 TypeScript 如何处理我们的代码,因此让我们创建一个名为 tsconfig.json 的新文件并添加以下内容

{
  "compilerOptions": {
    "target": "esnext",
    "module": "esnext",
    "moduleResolution": "node",
    "lib": ["es2017", "dom"],
    "allowJs": true,
    "checkJs": true,
    "noEmit": true,
    "strict": false,
    "noImplicitThis": true,
    "alwaysStrict": true,
    "esModuleInterop": true
  },
  "include": [ "script", "test" ],
  "exclude": [ "node_modules" ]
}

对于我们的目的,此配置文件的重要行是 allowJscheckJs 选项,它们都设置为 true。这些告诉 TypeScript 我们希望它评估我们的 JavaScript 代码。我们还告诉 TypeScript 检查 /script 目录中的所有文件,因此让我们创建该目录并在其中创建一个名为 index.js 的新文件。

一个简单的例子

在我们新创建的 JavaScript 文件中,让我们创建一个简单的加法函数,它接受两个参数并将它们加在一起

function add(x, y) {
  return x + y;
}

相当简单,对吧?add(4, 2) 将返回 6,但由于 JavaScript 是 动态类型的,您也可以使用字符串和数字调用 add 并获得一些可能意想不到的结果

add('4', 2); // returns '42'

这不太理想。幸运的是,我们可以向我们的函数添加一些 JSDoc 注释来告诉用户我们期望它如何工作

/**
 * Add two numbers together
 * @param {number} x
 * @param {number} y
 * @return {number}
 */
function add(x, y) {
  return x + y;
}

我们没有更改代码;我们只是添加了一条注释来告诉用户该函数的预期使用方法以及应期望返回什么值。我们通过利用 JSDoc 的 @param@return 注释并在花括号 ({}) 中设置类型来做到这一点。

尝试运行我们之前不正确的代码段会在 VS Code 中引发错误

TypeScript 评估对 add 的调用是不正确的,如果其中一个参数是字符串。

在上面的示例中,TypeScript 正在读取我们的注释并为我们检查它。在实际的 TypeScript 中,我们的函数现在等效于编写

/**
 * Add two numbers together
 */
function add(x: number, y: number): number {
  return x + y;
}

就像我们使用 number 类型一样,我们还可以使用 JSDoc 访问数十种内置类型,包括字符串、对象、数组以及许多其他类型,如 HTMLElementMutationRecord 等。

使用 JSDoc 注释而不是 TypeScript 专有语法的额外好处之一是,它为开发人员提供了机会通过内联提供有关参数或类型定义的其他元数据(希望鼓励养成自我记录代码的良好习惯)。

我们还可以告诉 TypeScript 某些对象的实例可能存在期望。例如,WeakMap 是一个内置的 JavaScript 对象,它在任何对象和任何其他数据之间创建映射。默认情况下,此第二段数据可以是任何内容,但如果我们希望我们的 WeakMap 实例仅将字符串作为值,我们可以告诉 TypeScript 我们想要什么

/** @type {WeakMap<object>, string} */
const metadata = new WeakMap();


const object = {};
const otherObject = {};


metadata.set(object, 42);
metadata.set(otherObject, 'Hello world');

当我们尝试将数据设置为 42 时,这会引发错误,因为它不是字符串。

定义我们自己的类型

就像 TypeScript 一样,JSDoc 允许我们定义和使用我们自己的类型。让我们创建一个名为 Person 的新类型,它具有 nameagehobby 属性。以下是 TypeScript 中的外观

interface Person {
  name: string;
  age: number;
  hobby?: string;
}

在 JSDoc 中,我们的类型如下所示

/**
 * @typedef Person
 * @property {string} name - The person's name
 * @property {number} age - The person's age
 * @property {string} [hobby] - An optional hobby
 */

我们可以使用 @typedef 标记定义我们类型的 name。让我们定义一个名为 Person 的接口,它具有必需的 name(一个字符串)和 age(一个数字)属性,以及一个第三个可选属性 hobby(一个字符串)。要定义这些属性,我们在注释中使用 @property(或简写 @prop 键)。

当我们选择使用 @type 注释将 Person 类型应用于新对象时,我们在编写代码时会获得类型检查和自动完成。不仅如此,当我们的对象不符合我们在文件中定义的约定时,我们也会被告知

Screenshot of an example of TypeScript throwing an error on our vanilla JavaScript object

现在,完成对象将清除错误

我们的对象现在符合上面定义的 Person 接口

但是,有时我们不希望为类型使用完整的对象。例如,我们可能希望提供一组有限的可能选项。在这种情况下,我们可以利用称为 联合类型 的东西

/**
 * @typedef {'cat'|'dog'|'fish'} Pet
 */


/**
 * @typedef Person
 * @property {string} name - The person's name
 * @property {number} age - The person's age
 * @property {string} [hobby] - An optional hobby
 * @property {Pet} [pet] - The person's pet
 */

在此示例中,我们定义了一个名为 Pet 的联合类型,它可以是 'cat''dog''fish' 的任何可能选项。我们所在区域的任何其他动物都不允许作为宠物,因此如果上面的 caleb 试图将 'kangaroo' 收养到他的家中,我们会收到错误消息

/** @type {Person} */
const caleb = {
  name: 'Caleb Williams',
  age: 33,
  hobby: 'Running',
  pet: 'kangaroo'
};
Screenshot of an an example illustrating that kangaroo is not an allowed pet type

同样的技术可以用来混合函数中的各种类型

/**
 * @typedef {'lizard'|'bird'|'spider'} ExoticPet
 */


/**
 * @typedef Person
 * @property {string} name - The person's name
 * @property {number} age - The person's age
 * @property {string} [hobby] - An optional hobby
 * @property {Pet|ExoticPet} [pet] - The person's pet
 */

现在我们的 person 类型可以具有 PetExoticPet

使用泛型

有时我们可能不希望使用严格的类型,而希望在编写一致的、强类型的代码时具有一定的灵活性。输入 泛型类型。泛型函数的经典示例是恒等函数,它接受一个参数并将其返回给用户。在 TypeScript 中,它看起来像这样

function identity<T>(target: T): T {
  return target;
}

在这里,我们定义了一个新的泛型类型 (T) 并告诉计算机和我们的用户该函数将返回一个与参数 target 的类型相同的类型的 value。这样,我们仍然可以传入数字或字符串或 HTMLElement 并确保返回值也属于相同的类型。

使用 JSDoc 符号,使用 @template 注释也可以实现相同的功能

/**
 * @template T
 * @param {T} target
 * @return {T}
 */
function identity(target) {
  return x;
}

泛型是一个复杂的话题,但有关如何在 JSDoc 中使用它们的更详细文档(包括示例),您可以阅读 Google Closure Compiler 关于该主题的页面

类型转换

虽然强类型通常非常有用,但您可能会发现 TypeScript 的内置期望并不完全适合您的用例。在这种情况下,我们可能需要将对象转换为新的类型。这可能是必需的一种情况是在处理事件侦听器时。

在 TypeScript 中,所有事件侦听器都将函数作为回调,其中第一个参数是类型为 Event 的对象,它具有一个名为 target 的属性,该属性为 EventTarget。这根据 DOM 标准的正确类型,但通常我们想要从事件目标中获取的信息并不存在于 EventTarget 上——例如存在于 HTMLInputElement.prototype 上的 value 属性。这使得以下代码无效

document.querySelector('input').addEventListener(event => {
  console.log(event.target.value);
};

TypeScript 会抱怨属性 value 不存在于 EventTarget 上,即使我们作为开发人员完全清楚 <input> 确实具有 value

A screenshot showing that value doesn’t exist on type EventTarget

为了让我们告诉 TypeScript 我们知道 event.target 将是 HTMLInputElement,我们必须转换对象的类型

document.getElementById('input').addEventListener('input', event => {
  console.log(/** @type {HTMLInputElement} */(event.target).value);
});

event.target 包含在括号中会将其与对 value 的调用分开。在括号之前添加类型将告诉 TypeScript 我们意味着 event.target 是与它通常期望的不同。

Screenshot of a valid example of type casting in VS Code.

如果某个特定对象有问题,我们始终可以告诉 TypeScript 一个对象是 @type {any} 以忽略错误消息,尽管这通常被认为是不好的做法,尽管在紧急情况下很有用。

总结

TypeScript 是一款非常强大的工具,许多开发人员正在使用它来简化其围绕一致代码标准的工作流程。虽然大多数应用程序都会使用内置的编译器,但一些项目可能会认为 TypeScript 提供的额外语法会妨碍开发。或者也许他们只是觉得坚持标准比绑定到扩展语法更舒服。在这些情况下,开发人员仍然可以获得利用 TypeScript 类型系统的好处,即使是在编写原生 JavaScript 时。