类型检查 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 | /** @type {number} */ |
你可以在后面找到受支持的 JSDoc 形式的完整清单.
由类内部赋值推断属性类型
ES2015 没有为类声明属性的途径. 属性如对象字面量一样被动态指定.
在 .js
文件中, 编译器由类内部的属性赋值推断属性的类型.
属性的类型就是在构造函数给定的类型, 除非这属性没在构造函数定义, 或那里的类型是 undefined 或 null.
那样的话, 属性的类型是所有赋值表达式右值类型的自适应类型.
我们假定在构造函数定义的属性总是存在, 而认为仅定义在方法, 取方法, 存方法的那些是”可选”的.
1 | class C { |
从未在类内部设置过的属性被认为是未知的.
如果你的类有一个只读不写的属性, 用 JSDoc 在构造函数添加并注解一个声明以指明它的类型.
如果它会稍后初始化, 你甚至不用在这里给初值:
1 | class C { |
构造器函数等同于类
在 ES2015 之前, JavaScript 用构造函数代替类.
编译器支持此模型, 而且理解构造函数和 ES2015 类是等同的.
上述属性推导规则照常发挥作用.
1 | function C() { |
支持 CommonJS 模块
在 .js
文件中, TypeScript 理解 CommonJS 模块格式.
向 exports
和 module.exports
赋值会被识别为导出声明.
类似地, require
函数调用会被识别为模块导入. 举个例子:
1 | // same as `import module "fs"` |
JavaScript 模块支持语法比 TypeScript 宽容得多.
大多赋值和声明的组合都受支持.
类, 函数, 对象字面量都是名字空间
.js
文件中类就是名字空间.
可以用它嵌套类, 举个例子:
1 | class C { |
而且, 针对 pre-ES2015 代码, 可以用它模拟静态方法:
1 | function Outer() { |
也可以用它创建简单的名字空间:
1 | var ns = {} |
更多其他变体:
1 | // IIFE |
对象字面量是开放式的
在 .ts
文件中, 初始化一个变量声明的对象字面量也把它的类型赋给声明.
不能再添加未在原字面量指定的新成员.
此规则在 .js
文件得以放松; 对象字面量类型有一个允许添加和查找非原始属性的开放式类型 (索引签名).
例如:
1 | var obj = { a: 1 }; |
对象字面量表现得它们好像有一个索引签名 [x:string]: any
一样, 这让它们可被当作开放的图, 而不是封闭对象.
类同其他特殊 JS 类型检查行为, 该行为可由为变量指定 JSDoc 类型所改变. 例如:
1 | /** @type {{a: number}} */ |
null, undefined, 空数组初始化器的类型是 any 或 any[]
任何以 null 或 undefined 初始化的变量, 参数, 或属性的类型都是 any, 即使严格 null 检查已经打开.
任何以 [] 初始化的变量, 参数, 或属性的类型都是 any[], 即使严格 null 检查已经打开.
唯一例外是上面提到的拥有多重初始化语句的属性.
1 | function Foo(i = null) { |
函数参数默认可选
由于 pre-ES2015 JavaSciprt 没有为参数指明”可选”性的方法, .js
文件所有的函数参数都被认为是”可选”的.
允许用少于声明数量的参数调用一个函数.
但用过多的参数调用一个函数仍是错误的.
例如:
1 | function bar(a, b) { |
有 JSDoc 注解的函数排除于此规则.
使用 JSDoc 可选参数语法表达”可选”性. 例如:
1 | /** |
由 arguments
的使用推断出变长参数声明
我们隐式地认为一个函数体引用过 arguments
的函数有一个变长参数 (即: (...arg: any[]) => any
). 使用 JSDoc var-arg 语法指定参数的类型.
1 | /** @param {...number} args */ |
未特化类型参数缺省为 any
JavaScript 没有天然的特化泛型类型参数语法, 未特化类型参数缺省为 any
.
在 extends 子句中
比如, React.Component
有 Props
和 State
两个类型参数.
在 .js
文件中, 没有合法的在 extends 子句特化它们的途径. 默认情况下这些类型参数的类型是 any
:
1 | import { Component } from "react"; |
使用 JSDoc @augments
显式指明类型. 例如:
1 | import { Component } from "react"; |
在 JSDoc 注解中
JSDoc 中的未特化类型参数缺省为 any:
1 | /** @type{Array} */ |
在函数调用中
对泛型函数的调用使用实参推导类型参数. 有时这个过程未能推出任何类型, 主要是因为缺乏推导源; 当这种情况发生, 类型参数也缺省为 any
, 例如:
1 | var p = new Promise((resolve, reject) => { reject() }); |
受支持的 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 | /** |
@type
能够指定自适应类型 — 比如, 能代表字符串或布尔值的某种东西.
1 | /** |
自适应类型的括号是可选的.
1 | /** |
你可以以多种语法指定数组类型:
1 | /** @type {number[]} */ |
你也可以指定对象字面量类型.
例如, 由属性 ‘a’ (字符串) 和 ‘b’ (数值) 构成的对象用到以下语法:
1 | /** @type {{ a: string, b: number }} */ |
可以以字符串或数值索引签名指明类图和类数组对象, 根据喜好选择标准 JSDoc 语法或 TypeScript 语法.
1 | /** |
以上两个类型分别等同于两 TypeScript 类型 { [x: string]: number }
和 { [x: number]: any }
. 编译器都能理解.
你可以遵循 TypeScript 或 Closure 语法指明函数类型:
1 | /** @type {function(string, boolean): number} Closure syntax */ |
你也可以使用不明确 Function
类型:
1 | /** @type {Function} */ |
其他来自 Closure 的类型:
1 | /** |
类型转换
TypeScripts 借鉴 Closure 的转型语法.
允许你通过在括号表达式前添加 @type
标签把一个类型转型成另一个.
1 | /** |
导入类型
你也可以使用导入类型从其他文件导入声明.
这语法是 TypeScript 特有的, 与 JSDoc 标准不同:
1 | /** |
导入类型可以用于类型别名声明:
1 | /** |
对一个类型不明确, 或难于声明的来自模块的值, 可以用导入类型获取它的类型:
1 | /** |
@param
和 @returns
@param
在保持与 @type
一致的类型语法基础上多了一个参数名.
用方括号环绕一个参数名声明参数是”可选”的:
1 | // Parameters may be declared in a variety of syntactic forms |
同样地, 指定函数返回值类型:
1 | /** |
@typedef
, @callback
, @param
我们用 @typedef
定义复杂类型.
同样的语法适用于 @param
.
1 | /** |
第一行的 Object
可用 object
替换.
1 | /** |
@param
允许使用相同语法声明一次性类型.
注意, 嵌套属性名必须加上参数名前缀.
1 | /** |
@callback
与 @typedef
相似, 只是它规定的是函数类型, 而不是对象类型.
1 | /** |
当然, 上面每个类型都可以采用 TypeScript 语法以单行 @typedef
声明:
1 | /** @typedef {{ prop1: string, prop2: string, prop3?: number }} SpecialType */ |
@template
你可以用 @template
标签声明泛型类型:
1 | /** |
用逗号或多标签声明多个类型参数:
1 | /** |
你也可以在类型参数名前面注明类型限制.
限制只对列表第一个类型参数有效:
1 | /** |
@constructor
编译器由 this 属性赋值语句推出构造函数, 但你可以添加 @constructor
标签使检查更严格, 建议更准确.
1 | /** |
有了 @constructor
标签, this
在构造函数 C
内部被检查, 于是你会获得对 initialize
方法的建议, 而且, 传给它数值将产生错误. 如果你不构造而是直接调用 C
, 也会产生错误.
不幸的是, 这意味着可调用的构造函数不能使用 @constructor
.
@this
借助上下文, 编译器通常能求出 this
的类型. 不然, 你可以用 @this
显式指定 this
的类型.
1 | /** |
@extends
从泛型基类派生 JavaScript 类, 我们没有地方指明类型参数会是什么. @extends
为类型参数提供位置:
1 | /** |
目前, @extends
只能用于类, 还没有构造函数继承类的方法.
@enum
@enum
标签允许你创建一个所有成员都来自特定类型的对象字面量. 不同于多数 JavaScript 对象字面量, 它不接收其他成员.
1 | /** @enum {number} */ |
@enum
明显区别于, 也很大程度地简单于, TypeScript 的 enum
. 不过, @enum
可有任意类型:
1 | /** @enum {function(number): number} */ |
更多实例
1 | var someObj = { |
已知不会受支持的形式
在值空间把对象视作类型行不通, 除非这对象创建类型, 如构造器函数.
1 | function aNormalFunction() { |
在对象字面量类型中为属性的类型添加等号后缀不会声明一个可选属性:
1 | /** |
可为空类型仅在 strictNullChecks
打开时有意义:
1 | /** |
非空类型被当作原类型, 无特殊含义:
1 | /** |
与 JSDoc 类型系统不同, TypeScript 仅允许你标记一个类型是否包含 null.
它不存在显式”非空”性 — 如果 strictNullChecks 开启, number
就不是可为空的.
如果关闭, number
就是可为空的.