类型检查 JavaScript 文件

状态: 初稿

TypeScript 2.3 及其后版本支持用 --checkJs 选项类型检查和报告 .js 文件中的错误.

你可以向一些文件添加 // @ts-nocheck 注释跳过对它们的检查; 相反, 你也能选择向一些文件添加 // @ts-check 注释在没设置 --checkJs 时只检查一部分 .js 文件.
你也可以添加 // @ts-ignore 于某行之前忽略该行错误.
如果你有 tsconfig.json 文件, JS 类型检查会服从 noImplicitAny, strictNullChecks 等严格标志.
但是, 由于 JS 类型检查相对的宽松性, 组合严格标志可能会产生出乎意料的结果.

这里是类型检查 .js 文件对比 .ts 文件的一些显著区别:

JSDoc 用以提供类型信息

.js 文件中, 类型通常按在 .ts 文件中一样被推导.
同样地, 如果一个类型无法被推导, 可以以 .ts 文件类型注解相同的方式使用 JSDoc 指明.
和 TypeScript 一样, --noImplicitAny 选项在编译器无法推出一个类型的地方产出错误.
(开放式对象字面量除外; 阅读后文获得更多信息.)

装饰一个声明的 JSDoc 注解可以指出那个声明的类型. 举例如下:

1
2
3
4
5
/** @type {number} */
var x;

x = 0; // OK
x = false; // Error: boolean is not assignable to number

你可以在后面找到受支持的 JSDoc 形式的完整清单.

由类内部赋值推断属性类型

ES2015 没有为类声明属性的途径. 属性如对象字面量一样被动态指定.

.js 文件中, 编译器由类内部的属性赋值推断属性的类型.
属性的类型就是在构造函数给定的类型, 除非这属性没在构造函数定义, 或那里的类型是 undefined 或 null.
那样的话, 属性的类型是所有赋值表达式右值类型的自适应类型.
我们假定在构造函数定义的属性总是存在, 而认为仅定义在方法, 取方法, 存方法的那些是”可选”的.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
class C {
constructor() {
this.constructorOnly = 0
this.constructorUnknown = undefined
}
method() {
this.constructorOnly = false // error, constructorOnly is a number
this.constructorUnknown = "plunkbat" // ok, constructorUnknown is string | undefined
this.methodOnly = 'ok' // ok, but methodOnly could also be undefined
}
method2() {
this.methodOnly = true // also, ok, methodOnly's type is string | boolean | undefined
}
}

从未在类内部设置过的属性被认为是未知的.
如果你的类有一个只读不写的属性, 用 JSDoc 在构造函数添加并注解一个声明以指明它的类型.
如果它会稍后初始化, 你甚至不用在这里给初值:

1
2
3
4
5
6
7
8
9
10
11
12
class C {
constructor() {
/** @type {number | undefined} */
this.prop = undefined;
/** @type {number | undefined} */
this.count;
}
}

let c = new C();
c.prop = 0; // OK
c.count = "string"; // Error: string is not assignable to number|undefined

构造器函数等同于类

在 ES2015 之前, JavaScript 用构造函数代替类.
编译器支持此模型, 而且理解构造函数和 ES2015 类是等同的.
上述属性推导规则照常发挥作用.

1
2
3
4
5
6
7
8
function C() {
this.constructorOnly = 0
this.constructorUnknown = undefined
}
C.prototype.method = function() {
this.constructorOnly = false // error
this.constructorUnknown = "plunkbat" // OK, the type is string | undefined
}

支持 CommonJS 模块

.js 文件中, TypeScript 理解 CommonJS 模块格式.
exportsmodule.exports 赋值会被识别为导出声明.
类似地, require 函数调用会被识别为模块导入. 举个例子:

1
2
3
4
5
6
7
// same as `import module "fs"`
const fs = require("fs");

// same as `export function readFile`
module.exports.readFile = function(f) {
return fs.readFileSync(f);
}

JavaScript 模块支持语法比 TypeScript 宽容得多.
大多赋值和声明的组合都受支持.

类, 函数, 对象字面量都是名字空间

.js 文件中类就是名字空间.
可以用它嵌套类, 举个例子:

1
2
3
4
class C {
}
C.D = class {
}

而且, 针对 pre-ES2015 代码, 可以用它模拟静态方法:

1
2
3
4
5
6
function Outer() {
this.y = 2
}
Outer.Inner = function() {
this.yy = 2
}

也可以用它创建简单的名字空间:

1
2
3
4
5
var ns = {}
ns.C = class {
}
ns.func = function() {
}

更多其他变体:

1
2
3
4
5
6
7
8
9
10
11
// IIFE
var ns = (function (n) {
return n || {};
})();
ns.CONST = 1

// defaulting to global
var assign = assign || function() {
// code goes here
}
assign.extra = 1

对象字面量是开放式的

.ts 文件中, 初始化一个变量声明的对象字面量也把它的类型赋给声明.
不能再添加未在原字面量指定的新成员.
此规则在 .js 文件得以放松; 对象字面量类型有一个允许添加和查找非原始属性的开放式类型 (索引签名).
例如:

1
2
var obj = { a: 1 };
obj.b = 2; // Allowed

对象字面量表现得它们好像有一个索引签名 [x:string]: any 一样, 这让它们可被当作开放的图, 而不是封闭对象.

类同其他特殊 JS 类型检查行为, 该行为可由为变量指定 JSDoc 类型所改变. 例如:

1
2
3
/** @type {{a: number}} */
var obj = { a: 1 };
obj.b = 2; // Error, type {a: number} does not have property b

null, undefined, 空数组初始化器的类型是 any 或 any[]

任何以 null 或 undefined 初始化的变量, 参数, 或属性的类型都是 any, 即使严格 null 检查已经打开.
任何以 [] 初始化的变量, 参数, 或属性的类型都是 any[], 即使严格 null 检查已经打开.
唯一例外是上面提到的拥有多重初始化语句的属性.

1
2
3
4
5
6
7
8
9
function Foo(i = null) {
if (!i) i = 1;
var j = undefined;
j = 2;
this.l = [];
}
var foo = new Foo();
foo.l.push(foo.i);
foo.l.push("end");

函数参数默认可选

由于 pre-ES2015 JavaSciprt 没有为参数指明”可选”性的方法, .js 文件所有的函数参数都被认为是”可选”的.
允许用少于声明数量的参数调用一个函数.

但用过多的参数调用一个函数仍是错误的.

例如:

1
2
3
4
5
6
7
function bar(a, b) {
console.log(a + " " + b);
}

bar(1); // OK, second argument considered optional
bar(1, 2);
bar(1, 2, 3); // Error, too many arguments

有 JSDoc 注解的函数排除于此规则.
使用 JSDoc 可选参数语法表达”可选”性. 例如:

1
2
3
4
5
6
7
8
9
10
11
/**
* @param {string} [somebody] - Somebody's name.
*/
function sayHello(somebody) {
if (!somebody) {
somebody = 'John Doe';
}
console.log('Hello ' + somebody);
}

sayHello();

arguments 的使用推断出变长参数声明

我们隐式地认为一个函数体引用过 arguments 的函数有一个变长参数 (即: (...arg: any[]) => any). 使用 JSDoc var-arg 语法指定参数的类型.

1
2
3
4
5
6
7
8
/** @param {...number} args */
function sum(/* numbers */) {
var total = 0
for (var i = 0; i < arguments.length; i++) {
total += arguments[i]
}
return total
}

未特化类型参数缺省为 any

JavaScript 没有天然的特化泛型类型参数语法, 未特化类型参数缺省为 any.

在 extends 子句中

比如, React.ComponentPropsState 两个类型参数.
.js 文件中, 没有合法的在 extends 子句特化它们的途径. 默认情况下这些类型参数的类型是 any:

1
2
3
4
5
6
7
import { Component } from "react";

class MyComponent extends Component {
render() {
this.props.b; // Allowed, since this.props is of type any
}
}

使用 JSDoc @augments 显式指明类型. 例如:

1
2
3
4
5
6
7
8
9
10
import { Component } from "react";

/**
* @augments {Component<{a: number}, State>}
*/
class MyComponent extends Component {
render() {
this.props.b; // Error: b does not exist on {a:number}
}
}

在 JSDoc 注解中

JSDoc 中的未特化类型参数缺省为 any:

1
2
3
4
5
6
7
8
9
10
11
/** @type{Array} */
var x = [];

x.push(1); // OK
x.push("string"); // OK, x is of type Array<any>

/** @type{Array.<number>} */
var y = [];

y.push(1); // OK
y.push("string"); // Error, string is not assignable to number

在函数调用中

对泛型函数的调用使用实参推导类型参数. 有时这个过程未能推出任何类型, 主要是因为缺乏推导源; 当这种情况发生, 类型参数也缺省为 any, 例如:

1
2
3
var p = new Promise((resolve, reject) => { reject() });

p; // Promise<any>;

受支持的 JSDoc

下表列出当使用 JSDoc 注解为 JavaScript 文件提供类型信息时, 目前哪些结构是受支持的.

而任何未显式列出的标签 (如 @async) 都还不受支持.

  • @type
  • @param (或 @arg@argument)
  • @returns (或 @return)
  • @typedef
  • @callback
  • @template
  • @class (或 @constructor)
  • @this
  • @extends (或 @augments)
  • @enum

它们的含义一般与在 usejsdoc.org 给出的标签相同, 或可作为它们的超集.
下面的代码描述可能差别, 给出每个标签的示例用法.

@type

你可以使用 “@type” 标签引用一个类型名 (原始类型, 用 TypeScript 声明的, 或 JSDoc “@typedef” 标签里的).
你可以使用任何 TypeScript 类型, 和多数 JSDoc 类型.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
/**
* @type {string}
*/
var s;

/** @type {Window} */
var win;

/** @type {PromiseLike<string>} */
var promisedString;

// You can specify an HTML Element with DOM properties
/** @type {HTMLElement} */
var myElement = document.querySelector(selector);
element.dataset.myData = '';

@type 能够指定自适应类型 — 比如, 能代表字符串或布尔值的某种东西.

1
2
3
4
/**
* @type {(string | boolean)}
*/
var sb;

自适应类型的括号是可选的.

1
2
3
4
/**
* @type {string | boolean}
*/
var sb;

你可以以多种语法指定数组类型:

1
2
3
4
5
6
/** @type {number[]} */
var ns;
/** @type {Array.<number>} */
var nds;
/** @type {Array<number>} */
var nas;

你也可以指定对象字面量类型.
例如, 由属性 ‘a’ (字符串) 和 ‘b’ (数值) 构成的对象用到以下语法:

1
2
/** @type {{ a: string, b: number }} */
var var9;

可以以字符串或数值索引签名指明类图和类数组对象, 根据喜好选择标准 JSDoc 语法或 TypeScript 语法.

1
2
3
4
5
6
7
8
9
/**
* A map-like object that maps arbitrary `string` properties to `number`s.
*
* @type {Object.<string, number>}
*/
var stringToNumber;

/** @type {Object.<number, object>} */
var arrayLike;

以上两个类型分别等同于两 TypeScript 类型 { [x: string]: number }{ [x: number]: any }. 编译器都能理解.

你可以遵循 TypeScript 或 Closure 语法指明函数类型:

1
2
3
4
/** @type {function(string, boolean): number} Closure syntax */
var sbn;
/** @type {(s: string, b: boolean) => number} Typescript syntax */
var sbn2;

你也可以使用不明确 Function 类型:

1
2
3
4
/** @type {Function} */
var fn7;
/** @type {function} */
var fn6;

其他来自 Closure 的类型:

1
2
3
4
5
6
7
8
/**
* @type {*} - can be 'any' type
*/
var star;
/**
* @type {?} - unknown type (same as 'any')
*/
var question;

类型转换

TypeScripts 借鉴 Closure 的转型语法.
允许你通过在括号表达式前添加 @type 标签把一个类型转型成另一个.

1
2
3
4
5
/**
* @type {number | string}
*/
var numberOrString = Math.random() < 0.5 ? "hello" : 100;
var typeAssertedNumber = /** @type {number} */ (numberOrString)

导入类型

你也可以使用导入类型从其他文件导入声明.
这语法是 TypeScript 特有的, 与 JSDoc 标准不同:

1
2
3
4
5
6
/**
* @param p { import("./a").Pet }
*/
function walk(p) {
console.log(`Walking ${p.name}...`);
}

导入类型可以用于类型别名声明:

1
2
3
4
5
6
7
8
9
/**
* @typedef { import("./a").Pet } Pet
*/

/**
* @type {Pet}
*/
var myPet;
myPet.name;

对一个类型不明确, 或难于声明的来自模块的值, 可以用导入类型获取它的类型:

1
2
3
4
/**
* @type {typeof import("./a").x }
*/
var x = require("./a").x;

@param@returns

@param 在保持与 @type 一致的类型语法基础上多了一个参数名.
用方括号环绕一个参数名声明参数是”可选”的:

1
2
3
4
5
6
7
8
9
10
11
// Parameters may be declared in a variety of syntactic forms
/**
* @param {string} p1 - A string param.
* @param {string=} p2 - An optional param (Closure syntax)
* @param {string} [p3] - Another optional param (JSDoc syntax).
* @param {string} [p4="test"] - An optional param with a default value
* @return {string} This is the result
*/
function stringsStringStrings(p1, p2, p3, p4){
// TODO
}

同样地, 指定函数返回值类型:

1
2
3
4
5
6
7
8
9
/**
* @return {PromiseLike<string>}
*/
function ps(){}

/**
* @returns {{ a: string, b: number }} - May use '@returns' as well as '@return'
*/
function ab(){}

@typedef, @callback, @param

我们用 @typedef 定义复杂类型.
同样的语法适用于 @param.

1
2
3
4
5
6
7
8
9
10
/**
* @typedef {Object} SpecialType - creates a new type named 'SpecialType'
* @property {string} prop1 - a string property of SpecialType
* @property {number} prop2 - a number property of SpecialType
* @property {number=} prop3 - an optional number property of SpecialType
* @prop {number} [prop4] - an optional number property of SpecialType
* @prop {number} [prop5=42] - an optional number property of SpecialType with default
*/
/** @type {SpecialType} */
var specialTypeObject;

第一行的 Object 可用 object 替换.

1
2
3
4
5
6
7
8
/**
* @typedef {object} SpecialType1 - creates a new type named 'SpecialType1'
* @property {string} prop1 - a string property of SpecialType1
* @property {number} prop2 - a number property of SpecialType1
* @property {number=} prop3 - an optional number property of SpecialType1
*/
/** @type {SpecialType1} */
var specialTypeObject1;

@param 允许使用相同语法声明一次性类型.
注意, 嵌套属性名必须加上参数名前缀.

1
2
3
4
5
6
7
8
9
10
11
/**
* @param {Object} options - The shape is the same as SpecialType above
* @param {string} options.prop1
* @param {number} options.prop2
* @param {number=} options.prop3
* @param {number} [options.prop4]
* @param {number} [options.prop5=42]
*/
function special(options) {
return (options.prop4 || 1001) + options.prop5;
}

@callback@typedef 相似, 只是它规定的是函数类型, 而不是对象类型.

1
2
3
4
5
6
7
8
/**
* @callback Predicate
* @param {string} data
* @param {number} [index]
* @returns {boolean}
*/
/** @type {Predicate} */
const ok = s => !(s.length % 2);

当然, 上面每个类型都可以采用 TypeScript 语法以单行 @typedef 声明:

1
2
/** @typedef {{ prop1: string, prop2: string, prop3?: number }} SpecialType */
/** @typedef {(data: string, index?: number) => boolean} Predicate */

@template

你可以用 @template 标签声明泛型类型:

1
2
3
4
5
6
/**
* @template T
* @param {T} x - A generic parameter that flows through to the return type
* @return {T}
*/
function id(x){ return x }

用逗号或多标签声明多个类型参数:

1
2
3
4
/**
* @template T,U,V
* @template W,X
*/

你也可以在类型参数名前面注明类型限制.
限制只对列表第一个类型参数有效:

1
2
3
4
5
6
7
8
9
/**
* @template {string} K - K must be a string or string literal
* @template {{ serious(): string }} Seriousalizable - must have a serious method
* @param {K} key
* @param {Seriousalizable} object
*/
function seriousalize(key, object) {
// ????
}

@constructor

编译器由 this 属性赋值语句推出构造函数, 但你可以添加 @constructor 标签使检查更严格, 建议更准确.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
/**
* @constructor
* @param {number} data
*/
function C(data) {
this.size = 0;
this.initialize(data); // Should error, initializer expects a string
}
/**
* @param {string} s
*/
C.prototype.initialize = function (s) {
this.size = s.length
}

var c = new C(0);
var result = C(1); // C should only be called with new

有了 @constructor 标签, this 在构造函数 C 内部被检查, 于是你会获得对 initialize 方法的建议, 而且, 传给它数值将产生错误. 如果你不构造而是直接调用 C, 也会产生错误.

不幸的是, 这意味着可调用的构造函数不能使用 @constructor.

@this

借助上下文, 编译器通常能求出 this 的类型. 不然, 你可以用 @this 显式指定 this 的类型.

1
2
3
4
5
6
7
/**
* @this {HTMLElement}
* @param {*} e
*/
function callbackForLater(e) {
this.clientHeight = parseInt(e) // should be fine!
}

@extends

从泛型基类派生 JavaScript 类, 我们没有地方指明类型参数会是什么. @extends 为类型参数提供位置:

1
2
3
4
5
6
7
/**
* @template T
* @extends {Set<T>}
*/
class SortableSet extends Set {
// ...
}

目前, @extends 只能用于类, 还没有构造函数继承类的方法.

@enum

@enum 标签允许你创建一个所有成员都来自特定类型的对象字面量. 不同于多数 JavaScript 对象字面量, 它不接收其他成员.

1
2
3
4
5
6
/** @enum {number} */
const JSDocState = {
BeginningOfLine: 0,
SawAsterisk: 1,
SavingComments: 2,
}

@enum 明显区别于, 也很大程度地简单于, TypeScript 的 enum. 不过, @enum 可有任意类型:

1
2
3
4
5
6
/** @enum {function(number): number} */
const Math = {
add1: n => n + 1,
id: n => -n,
sub1: n => n - 1,
}

更多实例

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
var someObj = {
/**
* @param {string} param1 - Docs on property assignments work
*/
x: function(param1){}
};

/**
* As do docs on variable assignments
* @return {Window}
*/
let someFunc = function(){};

/**
* And class methods
* @param {string} greeting The greeting to use
*/
Foo.prototype.sayHi = (greeting) => console.log("Hi!");

/**
* And arrow functions expressions
* @param {number} x - A multiplier
*/
let myArrow = x => x * x;

/**
* Which means it works for stateless function components in JSX too
* @param {{a: string, b: number}} test - Some param
*/
var fc = (test) => <div>{test.a.charAt(0)}</div>;

/**
* A parameter can be a class constructor, using Closure syntax.
*
* @param {{new(...args: any[]): object}} C - The class to register
*/
function registerClass(C) {}

/**
* @param {...string} p1 - A 'rest' arg (array) of strings. (treated as 'any')
*/
function fn10(p1){}

/**
* @param {...string} p1 - A 'rest' arg (array) of strings. (treated as 'any')
*/
function fn9(p1) {
return p1.join();
}

已知不会受支持的形式

在值空间把对象视作类型行不通, 除非这对象创建类型, 如构造器函数.

1
2
3
4
5
6
7
8
9
10
11
12
function aNormalFunction() {

}
/**
* @type {aNormalFunction}
*/
var wrong;
/**
* Use 'typeof' instead:
* @type {typeof aNormalFunction}
*/
var right;

在对象字面量类型中为属性的类型添加等号后缀不会声明一个可选属性:

1
2
3
4
5
6
7
8
9
/**
* @type {{ a: string, b: number= }}
*/
var wrong;
/**
* Use postfix question on the property name instead:
* @type {{ a: string, b?: number }}
*/
var right;

可为空类型仅在 strictNullChecks 打开时有意义:

1
2
3
4
5
6
/**
* @type {?number}
* With strictNullChecks: true -- number | null
* With strictNullChecks: off -- number
*/
var nullable;

非空类型被当作原类型, 无特殊含义:

1
2
3
4
5
/**
* @type {!number}
* Just has type number
*/
var normal;

与 JSDoc 类型系统不同, TypeScript 仅允许你标记一个类型是否包含 null.
它不存在显式”非空”性 — 如果 strictNullChecks 开启, number 就不是可为空的.
如果关闭, number 就是可为空的.

如果这篇文章对您有用,可以考虑打赏:)