VVLL.net

JavaScript文件类型检查

日期:2024-08-22 09:58:31

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的。

参考

标签