JSDoc支持_TypeScript笔记19

一.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条评论

发表评论

电子邮件地址不会被公开。 必填项已用*标注

*

code