一.JSDoc 与类型检查
.js
文件里不支持 TypeScript 类型标注语法:
// 错误 'types' can only be used in a .ts file.
let x: number;
因此,对于.js
文件,需要一种被 JavaScript 语法所兼容的类型标注方式,比如JSDoc:
/** @type {number} */
let x;
// 错误 Type '"string"' is not assignable to type 'number'.
x = 'string';
通过这种特殊形式(以/**
开头)的注释来表达类型,从而兼容 JavaScript 语法。TypeScript 类型系统解析这些 JSDoc 标记得到额外类型信息输入,并结合类型推断对.js
文件进行类型检查
P.S.关于.js
类型检查的更多信息,见检查 JavaScript 文件_TypeScript 笔记 18
二.支持程度
TypeScript 目前(2019/5/12)仅支持部分 JSDoc 标记,具体如下:
@type
:描述对象@param
(或@arg
或@argument
):描述函数参数@returns
(或@return
):描述函数返回值@typedef
:描述自定义类型@callback
:描述回调函数@class
(或@constructor
):表示该函数应该通过new
关键字来调用@this
:描述此处this
指向@extends
(或@augments
):描述继承关系@enum
:描述一组关联属性@property
(或@prop
):描述对象属性
P.S.完整的 JSDoc 标记列表见Block Tags
特殊的,对于泛型,JSDoc 里没有提供合适的标记,因此扩展了额外的标记:
@template
:描述泛型
P.S.用@template
标记描述泛型源自Google Closure Compiler,更多相关讨论见Add support for @template JSDoc
三.类型标注语法
TypeScript 兼容 JSDoc 类型标注,同时也支持在 JSDoc 标记中使用 TypeScript 类型标注语法:
The meaning is usually the same, or a superset, of the meaning of the tag given at usejsdoc.org.
没错,又是超集,因此any
类型有 3 种标注方式:
// JSDoc类型标注语法
/** @type {*} - can be 'any' type */
var star = true;
/** @type {?} - unknown type (same as 'any') */
var question = true;
// 都等价于TypeScript类型标注语法
/** @type {any} */
var thing = true;
语法方面,JSDoc 大多借鉴自Google Closure Compiler 类型标注,而 TypeScript 则有自己的一套类型语法,因此二者存在一些差异
类型声明
用@typedef
标记来声明自定义类型,例如:
/**
* @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;
等价于以下 TypeScript 代码:
type SpecialType = {
prop1: string;
prop2: number;
prop3?: number;
prop4?: number;
prop5?: number;
}
let specialTypeObject: SpecialType;
类型引用
通过@type
标记来引用类型名,类型名可以是基本类型,也可以是定义在 TypeScript 声明文件(d.ts)里或通过 JSDoc 标记@typedef
来定义的类型
例如:
// 基本类型
/**
* @type {string}
*/
var s;
/** @type {number[]} */
var ns;
/** @type {Array.<number>} */
var nds;
/** @type {Array<number>} */
var nas;
/** @type {Function} */
var fn7;
/** @type {function} */
var fn6;
// 定义在外部声明文件中的类型
/** @type {Window} */
var win;
/** @type {PromiseLike<string>} */
var promisedString;
/** @type {HTMLElement} */
var myElement = document.querySelector('#root');
element.dataset.myData = '';
// JSDoc @typedef 定义的类型
/** @typedef {(data: string, index?: number) => boolean} Predicate */
/** @type Predicate */
var p;
p('True or not ?');
对象类型也通过对象字面量来描述,索引签名同样适用:
/** @type {{ a: string, b: number }} */
var obj;
obj.a.toLowerCase();
/**
* 字符串索引签名
* @type {Object.<string, number>}
*/
var stringToNumber;
// 等价于
/** @type {{ [x: string]: number; }} */
var stringToNumber;
// 数值索引签名
/** @type {Object.<number, object>} */
var arrayLike;
// 等价于
/** @type {{ [x: number]: any; }} */
var arrayLike;
函数类型也有两种语法可选:
/** @type {function(string, boolean): number} Closure syntax */
var sbn;
/** @type {(s: string, b: boolean) => number} Typescript syntax */
var sbn2;
前者可以省掉形参名称,后者可以省去function
关键字,含义相同
同样支持类型组合:
// 联合类型(JSDoc类型语法)
/**
* @type {(string | boolean)}
*/
var sb;
// 联合类型(TypeScript类型语法)
/**
* @type {string | boolean}
*/
var sb;
二者等价,只是语法略有差异
跨文件类型引用
特殊的,能够通过import
引用定义在其它文件中的类型:
// a.js
/**
* @typedef Pet
* @property name {string}
*/
module.exports = {/* ... */};
// index.js
// 1.引用类型
/**
* @param p { import("./a").Pet }
*/
function walk(p) {
console.log(`Walking ${p.name}...`);
}
// 1.引用类型,同时起别名
/**
* @typedef { import("./a").Pet } Pet
*/
/**
* @type {Pet}
*/
var myPet;
myPet.name;
// 3.引用推断出的类型
/**
* @type {typeof import("./a").x }
*/
var x = require("./a").x;
注意,这种语法是 TypeScript 特有的(JSDoc 并不支持),而 JSDoc 中采用 ES Module 引入语法:
// a.js
/**
* @typedef State
* @property {Array} layers
* @property {object} product
*/
// index.js
import * as A from './a';
/** @param {A.State} state */
const f = state => ({
product: state.product,
layers: state.layers,
});
这种方式会添加实际的import
,如果是个纯粹的类型声明文件(只含有@typedef
的.js
,类似于d.ts
),JSDoc 方式会引入一个无用文件(只含有注释),而 TypeScript 方式则不存在这个问题
P.S.TypeScript 同时兼容这两种类型引入语法,更多相关讨论见Question: Import typedef from another file?
类型转换
类型转换(TypeScript 里的类型断言)语法与 JSDoc 一致,通过圆括号前的@type
标记说明圆括号里表达式的类型:
/** @type {!MyType} */ (valueExpression)
例如:
/** @type {number | string} */
var numberOrString = Math.random() < 0.5 ? "hello" : 100;
var typeAssertedNumber = /** @type {number} */ (numberOrString)
// 错误 Type '"hello"' is not assignable to type 'number'.
typeAssertedNumber = 'hello';
P.S.注意,必须要有圆括号,否则不认
四.常见类型
对象
一般用@typedef
标记用来描述对象类型,例如:
/**
* The complete Triforce, or one or more components of the Triforce.
* @typedef {Object} WishGranter~Triforce
* @property {boolean} hasCourage - Indicates whether the Courage component is present.
* @property {boolean} hasPower - Indicates whether the Power component is present.
* @property {boolean} hasWisdom - Indicates whether the Wisdom component is present.
*/
等价于 TypeScript 类型:
interface WishGranter {
hasCourage: boolean;
hasPower: boolean;
hasWisdom: boolean;
}
// 或
type WishGranter = {
hasCourage: boolean;
hasPower: boolean;
hasWisdom: boolean;
}
如果只是一次性的类型声明(无需复用,不想额外定义类型),可以用@param
标记来声明,通过options.prop1
形式的属性名来描述成员属性嵌套关系:
/**
* @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;
}
函数
类似于用@typedef
标记描述对象,可以用@callback
标记来描述函数的类型:
/**
* @callback Predicate
* @param {string} data
* @param {number} [index]
* @returns {boolean}
*/
/** @type {Predicate} */
const ok = s => !(s.length % 2);
等价于 TypeScript 代码:
type Predicate = (data: string, index?: number) => boolean
还可以用@typedef
特殊语法(仅 TypeScript 支持,JSDoc 里没有)把对象或函数的类型定义整合到一行:
/** @typedef {{ prop1: string, prop2: string, prop3?: number }} SpecialType */
/** @typedef {(data: string, index?: number) => boolean} Predicate */
// 等价于TypeScript代码
type SpecialType = {
prop1: string;
prop2: string;
prop3?: number;
}
type Predicate = (data: string, index?: number) => boolean
参数
函数参数通过@param
标记来描述,与@type
语法相同,只是增加了一个参数名,例如:
/**
* @param {string} p1 一个必填参数
*/
function f(p1) {}
而可选参数有 3 种表示方式:
/**
* @param {string=} p1 - 可选参数(Closure语法)
* @param {string} [p2] - 可选参数(JSDoc语法)
* @param {string} [p3 = 'test'] - 有默认值的可选参数(JSDoc语法)
*/
function fn(p1, p2, p3) {}
P.S.注意,后缀等号语法(如{string=}
)不适用于对象字面量类型,例如@type {{ a: string, b: number= }}
是非法的类型声明,可选属性应该用属性名后缀?
来表达
不定参数则有 2 种表示方式:
/**
* @param {...string} p - A 'rest' arg (array) of strings. (treated as 'any')
*/
function fn(p){ arguments; }
/** @type {(...args: any[]) => void} */
function f() { arguments; }
返回值
返回值的类型标注方式也类似:
/**
* @return {PromiseLike<string>}
*/
function ps() {
return Promise.resolve('');
}
/**
* @returns {{ a: string, b: number }}
*/
function ab() {
return {a: 'a', b: 11};
}
P.S.@returns
与@return
完全等价,后者是前者的别名
类
构造函数
类型系统会根据对this
的属性赋值推断出构造函数,也可以通过@constructor
标记来描述构造函数
二者区别在于有@constructor
标记时,类型检查更严格一些。具体的,会对构造函数中的this
属性访问以及构造函数参数进行检查,并且不允许(不通过new
关键字)直接调用构造函数:
/**
* @constructor
* @param {number} data
*/
function C(data) {
this.size = 0;
// 错误 Argument of type 'number' is not assignable to parameter of type 'string'.
this.initialize(data);
}
/**
* @param {string} s
*/
C.prototype.initialize = function (s) {
this.size = s.length
}
var c = new C(0);
// 错误 Value of type 'typeof C' is not callable. Did you mean to include 'new'?
var result = C(1);
P.S.去掉@constructor
标记的话,不会报出这两个错误
另外,对于构造函数或类类型的参数,可以通过类似于 TypeScript 语法的方式来描述其类型:
/**
* @template T
* @param {{new(): T}} C 要求构造函数C必须返回同一类(或子类)的实例
* @returns {T}
*/
function create(C) {
return new C();
}
P.S.JSDoc 没有提供描述 Newable 参数的方式,具体见Document class types/constructor types
this 类型
大多数时候类型系统能够根据上下文推断出this
的类型,对于复杂的场景可以通过@this
标记来显式指定this
的类型:
// 推断类型为 function getNodeHieght(): any
function getNodeHieght() {
return this.innerHeight;
}
// 显式指定this类型,推断类型为 function getNodeHieght(): number
/**
* @this {HTMLElement}
*/
function getNodeHieght() {
return this.clientHeight;
}
继承
TypeScript 里,类继承关系无法通过 JSDoc 来描述:
class Animal {
alive = true;
move() {}
}
/**
* @extends {Animal}
*/
class Duck {}
// 错误 Property 'move' does not exist on type 'Duck'.
new Duck().move();
@augments
(或@extends
)仅用来指定基类的泛型参数:
/**
* @template T
*/
class Box {
/**
* @param {T} value
*/
constructor(value) {
this.value = value;
}
unwrap() {
return this.value;
}
}
/**
* @augments {Box<string>} 描述
*/
class StringBox extends Box {
constructor() {
super('string');
}
}
new StringBox().unwrap().toUpperCase();
但与 JSDoc 不同的是,@arguments/extends
标记只能用于 Class,构造函数不适用:
/**
* @constructor
*/
function Animal() {
this.alive = true;
}
/**
* @constructor
* @augments Animal
*/
// 错误 JSDoc '@augments' is not attached to a class.
function Duck() {}
Duck.prototype = new Animal();
因此,@augments/extends
标记的作用很弱,既无法描述非 Class 继承,也不能决定继承关系(继承关系由extends
子句决定,JSDoc 描述的不算)
枚举
枚举用@enum
标记来描述,但与TypeScript 枚举类型不同,主要差异在于:
要求枚举成员类型一致
但枚举成员可以是任意类型
例如:
/** @enum {number} */
const JSDocState = {
BeginningOfLine: 0,
SawAsterisk: 1,
SavingComments: 2,
}
/** @enum {function(number): number} */
const SimpleMath = {
add1: n => n + 1,
id: n => n,
sub1: n => n - 1,
}
泛型
泛型用@template
标记来描述:
/**
* @template T
* @param {T} x - A generic parameter that flows through to the return type
* @return {T}
*/
function id(x) { return x }
let x = id('string');
// 错误 Type '0' is not assignable to type 'string'.
x = 0;
等价于 TypeScript 代码:
function id<T>(x: T): T {
return x;
}
let x = id('string');
x = 0;
有多个类型参数时,可以用逗号隔开,或者用多个@template
标签:
/**
* @template T, U
* @param {[T, U]} pairs 二元组
* @returns {[U, T]}
*/
function reversePairs(pairs) {
const x = pairs[0];
const y = pairs[1];
return [y, x];
}
// 等价于
/**
* @template T
* @template U
* @param {[T, U]} pairs 二元组
* @returns {[U, T]}
*/
function reversePairs(pairs) {
const x = pairs[0];
const y = pairs[1];
return [y, x];
}
此外,还支持泛型约束:
/**
* @typedef Lengthwise
* @property length {number}
*/
/**
* @template {Lengthwise} T
* @param {T} arg
* @returns {T}
*/
function loggingIdentity(arg) {
console.log(arg.length); // Now we know it has a .length property, so no more error
return arg;
}
等价于 TypeScript 代码:
interface Lengthwise {
length: number;
}
function loggingIdentity<T extends Lengthwise>(arg: T): T {
console.log(arg.length); // Now we know it has a .length property, so no more error
return arg;
}
特殊的,在结合@typedef
标记定义泛型类型时,必须先定义泛型参数:
/**
* @template K
* @typedef Wrapper
* @property value {K}
*/
/** @type {Wrapper<string>} */
var s;
s.value.toLocaleLowerCase();
@template
与@typedef
顺序不能反,否则报错:
JSDoc ‘@typedef’ tag should either have a type annotation or be followed by ‘@property’ or ‘@member’ tags.
等价于 TypeScript 泛型声明:
type Wrapper<K> = {
value: K;
}
Nullable
JSDoc 中,可以显式指定可 Null 类型与非 Null 类型,例如:
{?number}
:表示number | null
{!number}
:表示number
而 TypeScript 里无法显式指定,类型是否含有 Null 只与--strictNullChecks
选项有关:
/**
* @type {?number}
* 开启 strictNullChecks 时,类型为 number | null
* 关闭 strictNullChecks 时,类型为 number
*/
var nullable;
/**
* @type {!number} 显式指定非Null无效,只与 strictNullChecks 选项有关
*/
var normal;
《JSDoc支持_TypeScript笔记19》上有1条评论