一。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;
暫無評論,快來發表你的看法吧