跳到主要內容
黯羽輕揚每天積累一點點

JSDoc 支持_TypeScript 筆記 19

免費2019-05-12#TypeScript#TypeScript JSDoc#TypeScript JSDoc extends#JSDoc跨文件引用类型#JSDoc泛型#JSDoc inline typedef

TypeScript 僅支持部分 JSDoc 標記,但類型標注語法卻是 JSDoc 的超集

一。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](/articles/檢查 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 則有自己的一套 [類型語法](/articles/基本類型-typescript 筆記 2/),因此二者存在一些差異

類型聲明

@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 裡的 [類型斷言](/articles/基本類型-typescript 筆記 2/#articleHeader4))語法與 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;

有多個 [類型參數](/articles/泛型-typescript 筆記 6/#articleHeader3) 時,可以用逗號隔開,或者用多個 @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];
}

此外,還支持 [泛型約束](/articles/泛型-typescript 筆記 6/#articleHeader8):

/**
 * @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;

參考資料

評論

暫無評論,快來發表你的看法吧

提交評論