Preface
TypeScript's type checking is not limited to .ts, also supports .js
But to ensure file content only contains standard JavaScript code, .js files are checked according to ES syntax specifications, therefore TypeScript type annotations are not allowed:
.jsfiles are still checked to ensure that they only include standard ECMAScript features; type annotations are only allowed in.tsfiles and are flagged as errors in.jsfiles.
So use JSDoc to add extra type information to 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.
At the same time, type checking for .js is relatively looser, different from .ts type checking, differences mainly concentrated in 3 aspects:
-
Type annotation methods
-
Default types
-
Type inference strategies
P.S. Due to loose strategy, strict check flags like noImplicitAny, strictNullChecks etc. are also not so reliable in .js
I. Enable Checking
--allowJs option allows compiling JavaScript files, but by default won't do type checking on these files. Unless also enabling --checkJs option, will check all .js files
| 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. |
Additionally, TypeScript also supports some special comments used to control type checking:
-
// @ts-nocheck: File level, skip type checking -
// @ts-check: File level, perform type checking -
// @ts-ignore: Line level, ignore type errors
These comments provide finer-grained type checking control, for example if only want to check some .js files, can not enable --checkJs option, only add // @ts-check comment at first line of some .js files
II. Type Annotation Methods
In .js files use JSDoc to annotate types, for example:
/**
* @type {number}
*/
var x;
x = 0;
// Error Type 'false' is not assignable to type 'number'.
x = false;
Note, JSDoc has requirements for comment format, only recognizes those starting with /**:
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.
(Excerpted from Adding documentation comments to your code)
Additionally, not all JSDoc tags are supported, whitelist see Supported JSDoc
III. Default Types
On the other hand, there exist大量惯用 "patterns" in JavaScript, so quite loose in default types, mainly manifested in 3 points:
-
Function parameters default to optional
-
Unspecified type parameters default to
any -
Loosely typed object literals
Function Parameters Default to Optional
All function parameters in .js files default to optional, so allows actual parameter count less than formal parameters, but still errors when there are extra parameters, for example:
function bar(a, b) {
console.log(a + " " + b);
}
bar(1);
bar(1, 2);
// Error Expected 0-2 arguments, but got 3.
bar(1, 2, 3);
Note, exceptions when marking parameters as required through 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);
}
// Error Expected 1-2 arguments, but got 0.
sayHello();
sayHello('Hello');
sayHello('Hello', 'there');
// Error Expected 1-2 arguments, but got 3.
sayHello('Hello', 'there', 'wooo');
According to JSDoc annotation, in above example greeting is required, somebody is optional, therefore no parameters and 3 parameters will error
Specially, ES6 can implicitly mark optional parameters through default parameters and rest parameters, for example:
/**
* @param {string} somebody - Somebody's name.
*/
function sayHello(somebody = 'John Doe') {
console.log('Hello ' + somebody);
}
// Correct
sayHello();
From JSDoc annotation (@param {string} somebody) somebody appears required, default parameter (somebody = 'John Doe') indicates somebody is optional, type system will synthesize these information for inference
Unspecified Type Parameters Default to any
JavaScript doesn't provide syntax for representing generic parameters, therefore unspecified type parameters all default to any type
Generics in JavaScript mainly appear in 2 forms:
-
Inheriting generic classes, creating Promise etc. (generic classes, Promise etc. defined in external
d.ts) -
Other custom generics (marking generic types through JSDoc)
For example:
// Inherit generic class - .js
import { Component } from 'react';
class MyComponent extends Component {
render() {
// Correct this.props.unknownProp is any type
return <div>{this.props.unknownProp}</div>
}
}
Where this.props has generic type:
React.Component<any, any, any>.props: Readonly<any> & Readonly<{
children?: React.ReactNode;
}>
Because in .js when not specifying generic parameter types, defaults to any, so no error. But same code in .tsx will error:
// .tsx
import { Component } from 'react';
class MyComponent extends Component {
render() {
// Error Property 'unknownProp' does not exist on type 'Readonly<{}> & Readonly<{ children?: ReactNode; }>'.
return <div>{this.props.unknownProp}</div>
}
}
Promise scenarios are also similar:
// .js
var p = new Promise((resolve, reject) => { reject(false) });
// p type is Promise<any>
p;
// .ts
const p = new Promise<boolean>((resolve, reject) => { reject(false) });
// p type is Promise<boolean>
p;
Besides this kind of generics from external declarations (d.ts), there's another kind of custom "JavaScript generics":
// .js declare generics, but don't fill type parameters
/** @type{Array} */
var x = [];
x.push(1); // OK
x.push("string"); // OK, x is of type Array<any>
// .js declare generics, while specifying type parameters
/** @type{Array.<number>} */
var y = [];
y.push(1); // OK
y.push("string"); // Error, string is not assignable to number
That is generics defined through JSDoc, if not specifying type parameters, defaults to any
Loosely Typed Object Literals
In .ts, when initializing variables with object literals simultaneously determines that variable's type, and doesn't allow adding new members to object literals, for example:
// .ts
// obj type is { a: number; }
let obj = { a: 1 };
// Error Property 'b' does not exist on type '{ a: number; }'.
obj.b = 2;
In .js it's relatively loose:
// .js
var obj = { a: 1 };
// Correct
obj.b = 2;
Just like having index signature[x:string]: any;
// .ts
let obj: { a: number; [x: string]: any } = { a: 1 };
obj.b = 2;
Similarly, in JavaScript can also mark its exact type through JSDoc:
// .js
/** @type {{a: number}} */
var obj = { a: 1 };
// Error Property 'b' does not exist on type '{ a: number; }'.
obj.b = 2;
IV. Type Inference Strategies
Type inference divides into assignment inference and contextual inference, for .js there are some targeted inference strategies
Assignment inference:
-
Class member assignment inference
-
Constructor functions equivalent to classes
-
null,undefined,[]assignment inference
Contextual inference:
-
Rest parameter inference
-
Module inference
-
Namespace inference
Class Member Assignment Inference
In .ts infers instance property types through initialization assignment in class member declarations:
// .ts
class Counter {
x = 0;
}
// Infer x type as number
new Counter().x++;
While ES6 Class doesn't provide syntax for declaring instance properties, class properties are created through dynamic assignment, can also infer for this JavaScript idiomatic "pattern", for example:
class C {
constructor() {
this.constructorOnly = 0;
this.constructorUnknown = undefined;
}
method() {
// Error Type 'false' is not assignable to type 'number'.
this.constructorOnly = false;
this.constructorUnknown = "plunkbat";
this.methodOnly = 'ok';
}
method2() {
this.methodOnly = true;
}
}
All property assignments in class declarations serve as basis for (class instance) type inference, so in above example C class instance type is:
// TypeScript
type C = {
constructorOnly: number;
constructorUnknown: string;
method: () => void;
method2: () => void;
methodOnly: string | boolean
}
Specific rules as follows:
-
Property types determined through property assignments in constructor
-
For properties not defined in constructor, or types are
undefinedornullin constructor (at this time isany), their type is union of all assigned right-side value types -
Properties defined in constructor are all considered definitely existing, those appearing elsewhere (such as member methods) are treated as optional
-
Properties not appearing in class declarations are all undefined, accessing them errors
Constructor Functions Equivalent to Classes
Additionally, before ES6, JavaScript used constructor functions instead of classes, TypeScript type system can also "understand" this pattern (constructor functions equivalent to ES6 Class), member assignment inference also applies:
function C() {
this.constructorOnly = 0;
this.constructorUnknown = undefined;
}
C.prototype.method = function() {
// Error Type 'false' is not assignable to type 'number'.
this.constructorOnly = false;
this.constructorUnknown = "plunkbat";
}
null, undefined, [] Assignment Inference
In .js, variables, parameters or properties with initial values of null, undefined are all treated as any type, those with initial value of [] are treated as any[] type, for example:
// .js
function Foo(i = null) {
// i type is any
if (!i) i = 1; // i type still any
var j = undefined; // j type is any
j = 2; // j type is any | number i.e. number
this.j = j;
this.l = []; // this.l type is any[]
}
var foo = new Foo();
foo.l.push(foo.j);
foo.l.push("end");
Similarly, when assigning multiple times, type is union of each value types
Rest Parameter Inference
In .js will infer whether rest parameters exist based on arguments usage, for example:
// .js
function sum() {
var total = 0
for (var i = 0; i < arguments.length; i++) {
total += arguments[i]
}
return total
}
// sum type is (...args: any[]) => number
sum(1, 2, 3);
Of course, can also declare rest parameters through 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 type is (...args: number[]) => number
sum(1, 2, 3);
Module Inference
In .js, for CommonJS modules, will recognize exports, module.exports property assignments as module exports (export), while require function calls correspond to module imports (import), for example:
// .js
// Equivalent to `import module "fs"`
const fs = require("fs");
// Equivalent to `export function readFile`
module.exports.readFile = function(f) {
return fs.readFileSync(f);
}
P.S. Actually, TypeScript's support for CommonJS modules is completed through this kind of type inference
Namespace Inference
In .js, classes, functions and object literals are all treated as namespaces, because they are very similar to namespaces (all have dual meaning of value and type, all support nesting, and all three can be used together). For example:
// .js
class C { }
C.D = class { }
// Or
function Cls() {}
Cls.D = function() {}
new C.D();
new Cls.D();
Especially object literals, before ES6 were originally used as namespaces:
var c = {};
ns.D = class {}
ns.F = function() {}
new c.D();
new c.F();
No comments yet. Be the first to share your thoughts.