除非您过去几年一直躲在石头底下(老实说,有时躲在石头底下感觉像是正确的事情),否则您可能听说过并且可能使用过 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" ]
}
对于我们的目的,此配置文件的重要行是 allowJs
和 checkJs
选项,它们都设置为 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 中引发错误

add
的调用是不正确的,如果其中一个参数是字符串。在上面的示例中,TypeScript 正在读取我们的注释并为我们检查它。在实际的 TypeScript 中,我们的函数现在等效于编写
/**
* Add two numbers together
*/
function add(x: number, y: number): number {
return x + y;
}
就像我们使用 number
类型一样,我们还可以使用 JSDoc 访问数十种内置类型,包括字符串、对象、数组以及许多其他类型,如 HTMLElement
、MutationRecord
等。
使用 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
的新类型,它具有 name
、age
和 hobby
属性。以下是 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
类型应用于新对象时,我们在编写代码时会获得类型检查和自动完成。不仅如此,当我们的对象不符合我们在文件中定义的约定时,我们也会被告知

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

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'
};

同样的技术可以用来混合函数中的各种类型
/**
* @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
类型可以具有 Pet
或 ExoticPet
。
使用泛型
有时我们可能不希望使用严格的类型,而希望在编写一致的、强类型的代码时具有一定的灵活性。输入 泛型类型。泛型函数的经典示例是恒等函数,它接受一个参数并将其返回给用户。在 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
。

为了让我们告诉 TypeScript 我们知道 event.target
将是 HTMLInputElement
,我们必须转换对象的类型
document.getElementById('input').addEventListener('input', event => {
console.log(/** @type {HTMLInputElement} */(event.target).value);
});
将 event.target
包含在括号中会将其与对 value
的调用分开。在括号之前添加类型将告诉 TypeScript 我们意味着 event.target
是与它通常期望的不同。

如果某个特定对象有问题,我们始终可以告诉 TypeScript 一个对象是 @type {any}
以忽略错误消息,尽管这通常被认为是不好的做法,尽管在紧急情况下很有用。
总结
TypeScript 是一款非常强大的工具,许多开发人员正在使用它来简化其围绕一致代码标准的工作流程。虽然大多数应用程序都会使用内置的编译器,但一些项目可能会认为 TypeScript 提供的额外语法会妨碍开发。或者也许他们只是觉得坚持标准比绑定到扩展语法更舒服。在这些情况下,开发人员仍然可以获得利用 TypeScript 类型系统的好处,即使是在编写原生 JavaScript 时。
感谢您的文章。
也许您可以分享一个示例,说明如何在文件之间导入 @typedef 定义的类型,以及可能导致哪些问题?
还有从 GraphQL 模式生成类型呢?您是否遇到过这个问题?
缺乏类型支持是我不喜欢 GraphQL 的最大原因。
您可以将
import('./filename').Type
放入 typedef 花括号内。**仅使用 js** 的唯一问题是**您必须导出某些内容**才能导出类型。
@Arda Aydın 您如何首先导出类型?
太棒了!
嗨,Caleb,好文章!我认为用于 WeakMap 的索引签名的示例代码缺少一个闭合尖括号。
在 identity(target) 示例中,不应该返回
target
吗?只是一处小修改:在您用于泛型的 JSDoc 示例中,您写了
return x
,但我认为您是想写return target
。不过,这是一篇很棒的 TS 入门文章!
编辑器中的主题非常吸引人。请问您使用的是哪个编辑器,以及使用了哪个主题?
好文章!但是您真的需要为此安装 TypeScript 吗?我在一些项目中以这种方式使用 JSDoc 和 TypeScript(或任何 npm 包),但仍然收到 VSCode 关于类型错误的警告。
抱歉,但如果 TypeScript “难以阅读”,那么 JSDoc 甚至更糟糕——它更冗长,而且至少在某些方面,TS 的语法比 JSDoc 更接近其他主流语言。
就类型检查器而言,在我看来,JSDoc 既不是最好的,也不是最适合初学者的选项——您在此处列出的功能的学习曲线大致相同,但使用更丰富的类型检查器(如 TypeScript)的长期回报使其值得任何关心类型安全的人使用。
VSCode 预装了 TypeScript。因此,您无需为 JSDoc 安装它。