1. 程式人生 > 實用技巧 >JavaScript檔案型別檢查

JavaScript檔案型別檢查

TypeScript 2.3以後的版本支援使用--checkJs.js檔案進行型別檢查和錯誤提示。

你可以通過新增// @ts-nocheck註釋來忽略型別檢查;相反,你可以通過去掉--checkJs設定並新增一個// @ts-check註釋來選則檢查某些.js檔案。 你還可以使用// @ts-ignore來忽略本行的錯誤。 如果你使用了tsconfig.json,JS檢查將遵照一些嚴格檢查標記,如noImplicitAnystrictNullChecks等。 但因為JS檢查是相對寬鬆的,在使用嚴格標記時可能會有些出乎意料的情況。

對比.js檔案和.ts檔案在型別檢查上的差異,有如下幾點需要注意:

用JSDoc型別表示型別資訊

.js檔案裡,型別可以和在.ts檔案裡一樣被推斷出來。 同樣地,當型別不能被推斷時,它們可以通過JSDoc來指定,就好比在.ts檔案裡那樣。 如同TypeScript,--noImplicitAny會在編譯器無法推斷型別的位置報錯。 (除了物件字面量的情況;後面會詳細介紹)

JSDoc註解修飾的宣告會被設定為這個宣告的型別。比如:

/** @type {number} */
var x;

x = 0;      // OK
x = false;  // Error: boolean is not assignable to number

你可以在這裡找到所有JSDoc支援的模式,JSDoc文件

屬性的推斷來自於類內的賦值語句

ES2015沒提供宣告類屬性的方法。屬性是動態賦值的,就像物件字面量一樣。

.js檔案裡,編譯器從類內部的屬性賦值語句來推斷屬性型別。 屬性的型別是在建構函式裡賦的值的型別,除非它沒在建構函式裡定義或者在建構函式裡是undefinednull。 若是這種情況,型別將會是所有賦的值的型別的聯合型別。 在建構函式裡定義的屬性會被認為是一直存在的,然而那些在方法,存取器裡定義的屬性被當成可選的。

class C {
    constructor() {
        this.constructorOnly = 0
        this.constructorUnknown = undefined
    }
    method() {
        this.constructorOnly = false // error, constructorOnly is a number
        this.constructorUnknown = "plunkbat" // ok, constructorUnknown is string | undefined
        this.methodOnly = 'ok'  // ok, but y could also be undefined
    }
    method2() {
        this.methodOnly = true  // also, ok, y's type is string | boolean | undefined
    }
}

如果一個屬性從沒在類內設定過,它們會被當成未知的。

如果類的屬性只是讀取用的,那麼就在建構函式裡用JSDoc宣告它的型別。 如果它稍後會被初始化,你甚至都不需要在建構函式裡給它賦值:

class C {
    constructor() {
        /** @type {number | undefined} */
        this.prop = undefined;
        /** @type {number | undefined} */
        this.count;
    }
}

let c = new C();
c.prop = 0;          // OK
c.count = "string";  // Error: string is not assignable to number|undefined

建構函式等同於類

ES2015以前,Javascript使用建構函式代替類。 編譯器支援這種模式並能夠將建構函式識別為ES2015的類。 屬性型別推斷機制和上面介紹的一致。

function C() {
    this.constructorOnly = 0
    this.constructorUnknown = undefined
}
C.prototype.method = function() {
    this.constructorOnly = false // error
    this.constructorUnknown = "plunkbat" // OK, the type is string | undefined
}

支援CommonJS模組

.js檔案裡,TypeScript能識別出CommonJS模組。 對exportsmodule.exports的賦值被識別為匯出宣告。 相似地,require函式呼叫被識別為模組匯入。例如:

// same as `import module "fs"`
const fs = require("fs");

// same as `export function readFile`
module.exports.readFile = function(f) {
  return fs.readFileSync(f);
}

對JavaScript檔案裡模組語法的支援比在TypeScript裡寬泛多了。 大部分的賦值和宣告方式都是允許的。

類,函式和物件字面量是名稱空間

.js檔案裡的類是名稱空間。 它可以用於巢狀類,比如:

class C {
}
C.D = class {
}

ES2015之前的程式碼,它可以用來模擬靜態方法:

function Outer() {
  this.y = 2
}
Outer.Inner = function() {
  this.yy = 2
}

它還可以用於建立簡單的名稱空間:

var ns = {}
ns.C = class {
}
ns.func = function() {
}

同時還支援其它的變化:

// 立即呼叫的函式表示式
var ns = (function (n) {
  return n || {};
})();
ns.CONST = 1

// defaulting to global
var assign = assign || function() {
  // code goes here
}
assign.extra = 1

物件字面量是開放的

.ts檔案裡,用物件字面量初始化一個變數的同時也給它聲明瞭型別。 新的成員不能再被新增到物件字面量中。 這個規則在.js檔案裡被放寬了;物件字面量具有開放的型別,允許新增並訪問原先沒有定義的屬性。例如:

var obj = { a: 1 };
obj.b = 2;  // Allowed

物件字面量的表現就好比具有一個預設的索引簽名[x:string]: any,它們可以被當成開放的對映而不是封閉的物件。

與其它JS檢查行為相似,這種行為可以通過指定JSDoc型別來改變,例如:

/** @type {{a: number}} */
var obj = { a: 1 };
obj.b = 2;  // Error, type {a: number} does not have property b

null,undefined,和空陣列的型別是any或any[]

任何用nullundefined初始化的變數,引數或屬性,它們的型別是any,就算是在嚴格null檢查模式下。 任何用[]初始化的變數,引數或屬性,它們的型別是any[],就算是在嚴格null檢查模式下。 唯一的例外是像上面那樣有多個初始化器的屬性。

function Foo(i = null) {
    if (!i) i = 1;
    var j = undefined;
    j = 2;
    this.l = [];
}
var foo = new Foo();
foo.l.push(foo.i);
foo.l.push("end");

函式引數是預設可選的

由於在ES2015之前無法指定可選引數,因此.js檔案裡所有函式引數都被當做是可選的。 使用比預期少的引數呼叫函式是允許的。

需要注意的一點是,使用過多的引數呼叫函式會得到一個錯誤。

例如:

function bar(a, b) {
  console.log(a + " " + b);
}

bar(1);       // OK, second argument considered optional
bar(1, 2);
bar(1, 2, 3); // Error, too many arguments

使用JSDoc註解的函式會被從這條規則裡移除。 使用JSDoc可選引數語法來表示可選性。比如:

/**
 * @param {string} [somebody] - Somebody's name.
 */
function sayHello(somebody) {
    if (!somebody) {
        somebody = 'John Doe';
    }
    console.log('Hello ' + somebody);
}

sayHello();

arguments推斷出的var-args引數宣告

如果一個函式的函式體內有對arguments的引用,那麼這個函式會隱式地被認為具有一個var-arg引數(比如:(...arg: any[]) => any))。使用JSDoc的var-arg語法來指定arguments的型別。

/** @param {...number} args */
function sum(/* numbers */) {
    var total = 0
    for (var i = 0; i < arguments.length; i++) {
      total += arguments[i]
    }
    return total
}

未指定的型別引數預設為any

由於JavaScript裡沒有一種自然的語法來指定泛型引數,因此未指定的引數型別預設為any

在extends語句中:

例如,React.Component被定義成具有兩個型別引數,PropsState。 在一個.js檔案裡,沒有一個合法的方式在extends語句裡指定它們。預設地引數型別為any

import { Component } from "react";

class MyComponent extends Component {
    render() {
        this.props.b; // Allowed, since this.props is of type any
    }
}

使用JSDoc的@augments來明確地指定型別。例如:

import { Component } from "react";

/**
 * @augments {Component<{a: number}, State>}
 */
class MyComponent extends Component {
    render() {
        this.props.b; // Error: b does not exist on {a:number}
    }
}

在JSDoc引用中:

JSDoc裡未指定的型別引數預設為any

/** @type{Array} */
var x = [];

x.push(1);        // OK
x.push("string"); // OK, x is of type Array<any>

/** @type{Array.<number>} */
var y = [];

y.push(1);        // OK
y.push("string"); // Error, string is not assignable to number

在函式呼叫中

泛型函式的呼叫使用arguments來推斷泛型引數。有時候,這個流程不能夠推斷出型別,大多是因為缺少推斷的源;在這種情況下,型別引數型別預設為any。例如:

var p = new Promise((resolve, reject) => { reject() });

p; // Promise<any>;

支援的JSDoc

下面的列表列出了當前所支援的JSDoc註解,你可以用它們在JavaScript檔案裡新增型別資訊。

注意,沒有在下面列出的標記(例如@async)都是還不支援的。

  • @type
  • @param (or @arg or @argument)
  • @returns (or @return)
  • @typedef
  • @callback
  • @template
  • @class (or @constructor)
  • @this
  • @extends (or @augments)
  • @enum

它們代表的意義與usejsdoc.org上面給出的通常是一致的或者是它的超集。 下面的程式碼描述了它們的區別並給出了一些示例。

@type

可以使用@type標記並引用一個型別名稱(原始型別,TypeScript裡宣告的型別,或在JSDoc裡@typedef標記指定的) 可以使用任何TypeScript型別和大多數JSDoc型別。

/**
 * @type {string}
 */
var s;

/** @type {Window} */
var win;

/** @type {PromiseLike<string>} */
var promisedString;

// You can specify an HTML Element with DOM properties
/** @type {HTMLElement} */
var myElement = document.querySelector(selector);
element.dataset.myData = '';

@type可以指定聯合型別—例如,stringboolean型別的聯合。

/**
 * @type {(string | boolean)}
 */
var sb;

注意,括號是可選的。

/**
 * @type {string | boolean}
 */
var sb;

有多種方式來指定陣列型別:

/** @type {number[]} */
var ns;
/** @type {Array.<number>} */
var nds;
/** @type {Array<number>} */
var nas;

還可以指定物件字面量型別。 例如,一個帶有a(字串)和b(數字)屬性的物件,使用下面的語法:

/** @type {{ a: string, b: number }} */
var var9;

可以使用字串和數字索引簽名來指定map-likearray-like的物件,使用標準的JSDoc語法或者TypeScript語法。

/**
 * A map-like object that maps arbitrary `string` properties to `number`s.
 *
 * @type {Object.<string, number>}
 */
var stringToNumber;

/** @type {Object.<number, object>} */
var arrayLike;

這兩個型別與TypeScript裡的{ [x: string]: number }{ [x: number]: any }是等同的。編譯器能識別出這兩種語法。

可以使用TypeScript或Closure語法指定函式型別。

/** @type {function(string, boolean): number} Closure syntax */
var sbn;
/** @type {(s: string, b: boolean) => number} Typescript syntax */
var sbn2;

或者直接使用未指定的Function型別:

/** @type {Function} */
var fn7;
/** @type {function} */
var fn6;

Closure的其它型別也可以使用:

/**
 * @type {*} - can be 'any' type
 */
var star;
/**
 * @type {?} - unknown type (same as 'any')
 */
var question;

轉換

TypeScript借鑑了Closure裡的轉換語法。 在括號表示式前面使用@type標記,可以將一種型別轉換成另一種型別

/**
 * @type {number | string}
 */
var numberOrString = Math.random() < 0.5 ? "hello" : 100;
var typeAssertedNumber = /** @type {number} */ (numberOrString)

匯入型別

可以使用匯入型別從其它檔案中匯入宣告。 這個語法是TypeScript特有的,與JSDoc標準不同:

/**
 * @param p { import("./a").Pet }
 */
function walk(p) {
    console.log(`Walking ${p.name}...`);
}

匯入型別也可以使用在類型別名宣告中:

/**
 * @typedef Pet { import("./a").Pet }
 */

/**
 * @type {Pet}
 */
var myPet;
myPet.name;

匯入型別可以用在從模組中得到一個值的型別。

/**
 * @type {typeof import("./a").x }
 */
var x = require("./a").x;

@param@returns

@param語法和@type相同,但增加了一個引數名。 使用[]可以把引數宣告為可選的:

// Parameters may be declared in a variety of syntactic forms
/**
 * @param {string}  p1 - A string param.
 * @param {string=} p2 - An optional param (Closure syntax)
 * @param {string} [p3] - Another optional param (JSDoc syntax).
 * @param {string} [p4="test"] - An optional param with a default value
 * @return {string} This is the result
 */
function stringsStringStrings(p1, p2, p3, p4){
  // TODO
}

函式的返回值型別也是類似的:

/**
 * @return {PromiseLike<string>}
 */
function ps(){}

/**
 * @returns {{ a: string, b: number }} - May use '@returns' as well as '@return'
 */
function ab(){}

@typedef, @callback, 和 @param

@typedef可以用來聲明覆雜型別。 和@param類似的語法。

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

可以在第一行上使用objectObject

/**
 * @typedef {object} SpecialType1 - 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
 */
/** @type {SpecialType1} */
var specialTypeObject1;

@param允許使用相似的語法。 注意,巢狀的屬性名必須使用引數名做為字首:

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

@callback@typedef相似,但它指定函式型別而不是物件型別:

/**
 * @callback Predicate
 * @param {string} data
 * @param {number} [index]
 * @returns {boolean}
 */
/** @type {Predicate} */
const ok = s => !(s.length % 2);

當然,所有這些型別都可以使用TypeScript的語法@typedef在一行上宣告:

/** @typedef {{ prop1: string, prop2: string, prop3?: number }} SpecialType */
/** @typedef {(data: string, index?: number) => boolean} Predicate */

@template

使用@template宣告泛型:

/**
 * @template T
 * @param {T} p1 - A generic parameter that flows through to the return type
 * @return {T}
 */
function id(x){ return x }

用逗號或多個標記來宣告多個型別引數:

/**
 * @template T,U,V
 * @template W,X
 */

還可以在引數名前指定型別約束。 只有列表的第一項型別引數會被約束:

/**
 * @template {string} K - K must be a string or string literal
 * @template {{ serious(): string }} Seriousalizable - must have a serious method
 * @param {K} key
 * @param {Seriousalizable} object
 */
function seriousalize(key, object) {
  // ????
}

@constructor

編譯器通過this屬性的賦值來推斷建構函式,但你可以讓檢查更嚴格提示更友好,你可以新增一個@constructor標記:

/**
 * @constructor
 * @param {number} data
 */
function C(data) {
  this.size = 0;
  this.initialize(data); // Should error, initializer expects a string
}
/**
 * @param {string} s
 */
C.prototype.initialize = function (s) {
  this.size = s.length
}

var c = new C(0);
var result = C(1); // C should only be called with new

通過@constructorthis將在建構函式C裡被檢查,因此你在initialize方法裡得到一個提示,如果你傳入一個數字你還將得到一個錯誤提示。如果你直接呼叫C而不是構造它,也會得到一個錯誤。

不幸的是,這意味著那些既能構造也能直接呼叫的建構函式不能使用@constructor

@this

編譯器通常可以通過上下文來推斷出this的型別。但你可以使用@this來明確指定它的型別:

/**
 * @this {HTMLElement}
 * @param {*} e
 */
function callbackForLater(e) {
    this.clientHeight = parseInt(e) // should be fine!
}

@extends

當JavaScript類繼承了一個基類,無處指定型別引數的型別。而@extends標記提供了這樣一種方式:

/**
 * @template T
 * @extends {Set<T>}
 */
class SortableSet extends Set {
  // ...
}

注意@extends只作用於類。當前,無法實現建構函式繼承類的情況。

@enum

@enum標記允許你建立一個物件字面量,它的成員都有確定的型別。不同於JavaScript裡大多數的物件字面量,它不允許新增額外成員。

/** @enum {number} */
const JSDocState = {
  BeginningOfLine: 0,
  SawAsterisk: 1,
  SavingComments: 2,
}

注意@enum與TypeScript的@enum大不相同,它更加簡單。然而,不同於TypeScript的列舉,@enum可以是任何型別:

/** @enum {function(number): number} */
const Math = {
  add1: n => n + 1,
  id: n => -n,
  sub1: n => n - 1,
}

更多示例

var someObj = {
  /**
   * @param {string} param1 - Docs on property assignments work
   */
  x: function(param1){}
};

/**
 * As do docs on variable assignments
 * @return {Window}
 */
let someFunc = function(){};

/**
 * And class methods
 * @param {string} greeting The greeting to use
 */
Foo.prototype.sayHi = (greeting) => console.log("Hi!");

/**
 * And arrow functions expressions
 * @param {number} x - A multiplier
 */
let myArrow = x => x * x;

/**
 * Which means it works for stateless function components in JSX too
 * @param {{a: string, b: number}} test - Some param
 */
var sfc = (test) => <div>{test.a.charAt(0)}</div>;

/**
 * A parameter can be a class constructor, using Closure syntax.
 *
 * @param {{new(...args: any[]): object}} C - The class to register
 */
function registerClass(C) {}

/**
 * @param {...string} p1 - A 'rest' arg (array) of strings. (treated as 'any')
 */
function fn10(p1){}

/**
 * @param {...string} p1 - A 'rest' arg (array) of strings. (treated as 'any')
 */
function fn9(p1) {
  return p1.join();
}

已知不支援的模式

在值空間中將物件視為型別是不可以的,除非物件建立了型別,如建構函式。

function aNormalFunction() {

}
/**
 * @type {aNormalFunction}
 */
var wrong;
/**
 * Use 'typeof' instead:
 * @type {typeof aNormalFunction}
 */
var right;

物件字面量屬性上的=字尾不能指定這個屬性是可選的:

/**
 * @type {{ a: string, b: number= }}
 */
var wrong;
/**
 * Use postfix question on the property name instead:
 * @type {{ a: string, b?: number }}
 */
var right;

Nullable型別只在啟用了strictNullChecks檢查時才啟作用:

/**
 * @type {?number}
 * With strictNullChecks: true -- number | null
 * With strictNullChecks: off  -- number
 */
var nullable;

Non-nullable型別沒有意義,以其原型別對待:

/**
 * @type {!number}
 * Just has type number
 */
var normal;

不同於JSDoc型別系統,TypeScript只允許將型別標記為包不包含null。 沒有明確的Non-nullable -- 如果啟用了strictNullChecks,那麼number是非null的。 如果沒有啟用,那麼number是可以為null的。