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

檢查 JavaScript 文件_TypeScript 筆記 18

免費2019-05-05#TypeScript#TypeScript check js file#checkJS not working#JavaScript类型检查#JavaScript严格类型

TypeScript 的類型檢查不僅限於 .ts,還支持 .js

寫在前面

TypeScript 的類型檢查不僅限於 .ts,還支持 .js

但為了確保文件內容只含有標準的 JavaScript 代碼.js 文件按照 ES 語法規範來檢查,因而不允許出現 TypeScript 類型標註:

.js files are still checked to ensure that they only include standard ECMAScript features; type annotations are only allowed in .ts files and are flagged as errors in .js files.

所以通過 JSDoc 來給 JavaScript 添加額外的類型信息:

JSDoc comments can be used to add some type information to your JavaScript code, see JSDoc Support documentation for more details about the supported JSDoc constructs.

同時,針對 .js 的類型檢查相對寬鬆一些,與 .ts 的類型檢查有所不同,差異主要集中在 3 方面:

  • 類型標註方式

  • 默認類型

  • 類型推斷策略

P.S. 由於寬鬆策略,noImplicitAnystrictNullChecks 等嚴格校驗標記在 .js 裡也不那麼可靠

一、開啟檢查

--allowJs 選項允許編譯 JavaScript 文件,但默認不會對這些文件做類型檢查。除非再開啓 --checkJs 選項,會對所有的 .js 文件進行校驗

OptionTypeDefaultDescription
--allowJsbooleanfalseAllow JavaScript files to be compiled.
--checkJsbooleanfalseReport errors in .js files. Use in conjunction with --allowJs.

另外,TypeScript 還支持一些用來控制類型檢查的特殊注釋:

  • // @ts-nocheck:文件級,跳過類型檢查

  • // @ts-check:文件級,進行類型檢查

  • // @ts-ignore:行級,忽略類型錯誤

這些注釋提供了更細粒度的類型檢查控制,比如只想檢查部分 .js 文件的話,可以不開啓 --checkJs 選項,僅在部分 .js 文件首行添上 // @ts-check 注釋

二、類型標註方式

.js 文件裡通過 JSDoc 來標註類型,例如:

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

x = 0;
// 報錯 Type 'false' is not assignable to type 'number'.
x = false;

注意,JSDoc 對注釋格式有要求,以 /** 開頭的才認:

JSDoc comments should generally be placed immediately before the code being documented. Each comment must start with a /** sequence in order to be recognized by the JSDoc parser. Comments beginning with /*, /***, or more than 3 stars will be ignored.

(摘自 Adding documentation comments to your code

另外,並非所有 JSDoc 標記都支持,白名單見 Supported JSDoc

三、默認類型

另一方面,JavaScript 裡存在大量慣用「模式」,所以在默認類型方面相當寬鬆,主要表現為 3 點:

  • 函數參數默認可選

  • 未指定的類型參數默認 any

  • 類型寬鬆的對象字面量

函數參數默認可選

.js 文件裡所有函數參數都默認可選,所以允許實參數量少於形參,但存在多餘參數時仍會報錯,例如:

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

bar(1);
bar(1, 2);
// 錯誤 Expected 0-2 arguments, but got 3.
bar(1, 2, 3);

注意,通過 JSDoc 標註了參數必填時例外:

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

// 錯誤 Expected 1-2 arguments, but got 0.
sayHello();
sayHello('Hello');
sayHello('Hello', 'there');
// 錯誤 Expected 1-2 arguments, but got 3.
sayHello('Hello', 'there', 'wooo');

根據 JSDoc 標註,上例中 greeting 必填,somebody 可選,因此無參和 3 參會報錯

特殊的,ES6 可以通過 [默認參數和不定參數](/articles/默認參數和不定參數-es6 筆記 4/) 來隱式標記 [可選參數](/articles/函數-typescript 筆記 5/#articleHeader4),例如:

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

// 正確
sayHello();

從 JSDoc 標註(@param {string} somebody)來看 somebody 必填,默認參數(somebody = 'John Doe')表明 somebody 可選,類型系統會綜合這些信息進行推斷

未指定的類型參數默認 any

JavaScript 沒有提供用來表示泛型參數的語法,因此未指定的類型參數都默認 any 類型

泛型在 JavaScript 中主要以 2 種形式出現:

  • 繼承泛型類,創建 Promise 等(泛型類、Promise 等定義在外部 d.ts 裡)

  • 其它自定義泛型(通過 JSDoc 標明泛型類型)

例如:

// 繼承泛型類 - .js
import { Component } from 'react';
class MyComponent extends Component {
  render() {
    // 正確 this.props.unknownProp 是 any 類型
    return <div>{this.props.unknownProp}</div>
  }
}

其中 this.props 具有泛型類型:

React.Component<any, any, any>.props: Readonly<any> & Readonly<{
  children?: React.ReactNode;
}>

因為在 .js 裡沒有指定泛型參數的類型時,默認為 any,所以不報錯。但同樣的代碼在 .tsx 裡會報錯:

// .tsx
import { Component } from 'react';
class MyComponent extends Component {
  render() {
    // 錯誤 Property 'unknownProp' does not exist on type 'Readonly<{}> & Readonly<{ children?: ReactNode; }>'.
    return <div>{this.props.unknownProp}</div>
  }
}

Promise 的場景也類似:

// .js
var p = new Promise((resolve, reject) => { reject(false) });
// p 類型為 Promise<any>
p;

// .ts
const p = new Promise<boolean>((resolve, reject) => { reject(false) });
// p 類型為 Promise<boolean>
p;

除了這種來自外部聲明(d.ts)的泛型外,還有一種自定義的*"JavaScript 泛型"*:

// .js 聲明泛型,但不填類型參數
/** @type{Array} */
var x = [];
x.push(1);        // OK
x.push("string"); // OK, x is of type Array<any>

// .js 聲明泛型,同時指定類型參數
/** @type{Array.<number>} */
var y = [];

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

即通過 JSDoc 定義的泛型,若未指定類型參數,就默認 any

類型寬鬆的對象字面量

.ts 裡,用對象字面量初始化變量的同時會確定該變量的類型,並且不允許往對象字面量上添加新成員,例如:

// .ts
// obj 類型為 { a: number; }
let obj = { a: 1 };
// 錯誤 Property 'b' does not exist on type '{ a: number; }'.
obj.b = 2;

.js 裡則相對寬鬆:

// .js
var obj = { a: 1 };
// 正確
obj.b = 2;

就像具有 索引簽名[x:string]: any 一樣;

// .ts
let obj: { a: number; [x: string]: any } = { a: 1 };
obj.b = 2;

同樣,在 JavaScript 也可以通過 JSDoc 標明其確切類型:

// .js
/** @type {{a: number}} */
var obj = { a: 1 };
// 錯誤 Property 'b' does not exist on type '{ a: number; }'.
obj.b = 2;

四、類型推斷策略

類型推斷分為賦值推斷與上下文推斷,對於 .js 有一些針對性的推斷策略

賦值推斷:

  • Class 成員賦值推斷

  • 構造函數等價於類

  • nullundefined[] 賦值推斷

上下文推斷:

  • 不定參數推斷

  • 模塊推斷

  • 命名空間推斷

Class 成員賦值推斷

.ts 裡通過類成員聲明中的初始化賦值來推斷實例屬性的類型:

// .ts
class Counter {
  x = 0;
}
// 推斷 x 類型為 number
new Counter().x++;

而 ES6 Class 沒有提供 聲明實例屬性的語法,類屬性通過動態賦值來創建,對於這種 JavaScript 慣用「模式」也能進行推斷,例如:

class C {
  constructor() {
    this.constructorOnly = 0;
    this.constructorUnknown = undefined;
  }
  method() {
    // 錯誤 Type 'false' is not assignable to type 'number'.
    this.constructorOnly = false;
    this.constructorUnknown = "plunkbat";
    this.methodOnly = 'ok';
  }
  method2() {
    this.methodOnly = true;
  }
}

class 聲明中的所有屬性賦值都會作為(類實例)類型推斷的依據,所以上例中 C 類實例的類型為:

// TypeScript
type C = {
  constructorOnly: number;
  constructorUnknown: string;
  method: () => void;
  method2: () => void;
  methodOnly: string | boolean
}

具體規則如下:

  • 屬性類型通過構造函數中的屬性賦值來確定

  • 對於沒在構造函數中定義,或者構造函數中類型為 undefinednull(此時為 any���的屬性,其類型為所有賦值中右側值類型的聯合

  • 定義在構造函數中的屬性都認為是一定存在的,其它地方(如成員方法)出現的都當作可選的

  • 類聲明中未出現的屬性都是未定義的,訪問就報錯

構造函數等價於類

另外,在 ES6 之前,JavaScript 裡用構造函數代替類,TypeScript 類型系統也能夠「理解」這種模式(構造函數等價於 ES6 Class),成員賦值推斷同樣適用:

function C() {
  this.constructorOnly = 0;
  this.constructorUnknown = undefined;
}
C.prototype.method = function() {
  // 錯誤 Type 'false' is not assignable to type 'number'.
  this.constructorOnly = false;
  this.constructorUnknown = "plunkbat";
}

nullundefined[] 賦值推斷

.js 裡,初始值為 nullundefined 的變量、參數或屬性都視為 any 類型,初始值為 [] 的則視為 any[] 類型,例如:

// .js
function Foo(i = null) {
  // i 類型為 any
  if (!i) i = 1;  // i 類型仍為 any
  var j = undefined;  // j 類型為 any
  j = 2;  // j 類型為 any | number 即 number
  this.j = j;
  this.l = [];  // this.l 類型為 any[]
}
var foo = new Foo();
foo.l.push(foo.j);
foo.l.push("end");

同樣,多次賦值時,類型為各值類型的聯合

不定參數推斷

.js 裡會根據 arguments 的使用情況來推斷是否存在 [不定參數](/articles/默認參數和不定參數-es6 筆記 4/#articleHeader2),例如:

// .js
function sum() {
  var total = 0
  for (var i = 0; i < arguments.length; i++) {
    total += arguments[i]
  }
  return total
}
// sum 類型為 (...args: any[]) => number
sum(1, 2, 3);

當然,也可以通過 JSDoc 聲明不定參數:

// .js
/** @param {...number} args */
function sum(/* numbers */) {
  var total = 0
  for (var i = 0; i < arguments.length; i++) {
    total += arguments[i]
  }
  return total
}
// sum 類型為 (...args: number[]) => number
sum(1, 2, 3);

模塊推斷

.js 裡,對於 CommonJS 模塊,會把 exportsmodule.exports 的屬性賦值識別為模塊導出(export),而 require 函數調用則對應到模塊引入(import),例如:

// .js
// 等價於 `import module "fs"`
const fs = require("fs");

// 等價於 `export function readFile`
module.exports.readFile = function(f) {
  return fs.readFileSync(f);
}

P.S. 實際上,TypeScript 對 CommonJS 模塊的支持就是通過這種類型推斷來完成的

命名空間推斷

.js 裡,類、函數和對象字面量都視為命名空間,因為它們與命名空間非常相似(都具有值和類型的雙重含義、都支持嵌套、並且三者能夠結合使用)。例如:

// .js
class C { }
C.D = class { }
// 或者
function Cls() {}
Cls.D = function() {}

new C.D();
new Cls.D();

尤其是對象字面量,在 ES6 之前本就用作命名空間:

var c = {};
ns.D = class {}
ns.F = function() {}

new c.D();
new c.F();

參考資料

評論

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

提交評論