检查JavaScript文件_TypeScript笔记18

写在前面

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文件进行校验

Option Type Default Description
--allowJs boolean false Allow JavaScript files to be compiled.
--checkJs boolean false Report 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 可以通过默认参数和不定参数来隐式标记可选参数,例如:

/**
 * @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的使用情况来推断是否存在不定参数,例如:

// .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();

参考资料

发表评论

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

*

code