TypeScript,作为JavaScript的超集,给JavaScript带来了强类型这个非常强大的特性,给前端的开发以及重构带来了很大的便利性。但是,即使TypeScript现在已经可以直接使用用JavaScript编写的模块了,有很多遗留项目想要立马迁移到TypeScript也并非易事。但是好消息是,TypeScript在2.3版本引入了Js Type Checking,从此,不需要任何对于已有代码的侵入,你也可以很好的享受TypeScript带来的一些特性了。

从迁移TypeScript说起

笔者所说的这个项目,是一个运行了接近五年的老项目,代码横跨es3到es6。而且代码风格由于项目的人员流动也各异。由于项目人数越来越多,以前留下的技术债造成的危害也越来越大。而重构,在这样一个臃肿且混乱的项目中显得举步维艰。由于JS的动态性,有的时候你甚至不知道这个方法是某个类下的实例方法还是JavaScript原生或是JQuery所提供的方法。 这个时候,TypeScript由于其提供的强类型能很好的规范代码,方便开发人员进行日常开发和重构,开始进入了我们的视野。 但是马上我们面对上了另一个难题,那就是,我们只想享受TypeScript强类型带来的类型推导的优势,并不想花大功夫去把整个代码库全部重构成TypeScript。 不过好在,TypeScript从2.3版本后就开始支持使用JsDoc的语法,以comment的形式给JavaScript文件提供强类型的支持。Type Checking JavaScript Files · TypeScript

如何开始使用Type Checking

首先你需要在你需要检查的JavaScript的文件头部显式的加上如下comment

1
// @ts-check

对变量进行类型检查

如果你需要声名一个变量的类型,你可以这样

1
2
3
4
5
/** @type {number} */
let x;

x = 0;      // OK
x = false;  // Error: boolean is not assignable to number
当你尝试给x赋一个bool值时,编辑器会提示你类型冲突。

对函数进行类型检查

当然某些时候我们最需要的其实是对于函数签名和返回值的强类型声名,以便我们在其他地方使用的时候不会传入错误的参数或者使用类型错误的函数返回值。TypeScript提供了三种语法来声名函数的类型.

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
// jsdoc standard syntax
// 声明一个函数
/** 
 * @param {string} foo 
 * @param {string} bar
 * @returns {string}
*/

function test(foo, bar) {
  return `${foo} and ${bar}`
}

// closure syntax
// 声明一个函数表达式
/** 
 * @type {function(string, string): string}
*/

let test; // test 必须符合定义的函数签名
test = (foo, bar, foobar) => '123' // 报错 函数不符合定义的签名
test = (foo, bar) => 1  // 报错 函数返回值不符合


// typescript like syntax
// 声明一个函数表达式
/** 
 * @type {(foo:string, bar:string) => string}
*/

let test; // test 必须符合定义的函数签名

在日常使用中,如果你想声明一个函数,对于函数做类型检查,你需要使用 @params @returns这种declaration 语法。 如果你想确定一个函数表达式的签名,你需要使用 @type的语法。

对于 @type 语法, 从表现力上,我个人更偏好typscript like syntax因为表达力最强。但是可惜的是这种语法在webstorm上会被认为是非法语法。

自定义类型

自定义类型有点类似于定义TypeScript中的Interface,TypeScript同样支持两种语法来自定义类型。

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
/** 
 * @typedef {Object} Human
 * @prop {string} name
 * @property {number} age
 * @prop {(string) => void} talk
 */

/** @type {Human} */
 let Human;

/** 
 * @typedef {{name: string, age:number, talk: (string) => void}} Person
 */

/** @type {Person}*/
let person;
需要注意的是,第二种语法在webstorm中也不被认为只一个合法的语法。

声明一个class

对于类的property的类型,可以很轻松的进行检查

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
// 有初始化值,可以依赖类型推导
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
    }
}

// 无初始化值,显式声明.
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

对于类的实例方法,你需要对于函数做出声明.

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
class Foo {
  /**
   * @param {string} binggo
   * @returns {string} 
  */
  bar(bingo) {
    return bingo;
  }
}

let test2 = new Foo()
test2.bar(1); // Error Emit.

其他的一些特性

可选参数

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
/**
 * @param {string} [somebody] - Somebody's name.
 */
function sayHello(somebody) {
    if (!somebody) {
        somebody = 'John Doe';
    }
    console.log('Hello ' + somebody);
}

sayHello();

类型union

1
2
3
4
/**
 * @type {(string | boolean)}
 */
var abc;

枚举

jsdoc的枚举和其他语言的枚举或者typescript的枚举有所不同,更多的是起到描述属性值为同一种类型的object的作用

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
/**
 * @enum {number}
*/
 
const JSDocState = {
  BeginningOfLine: 0,
  SawAsterisk: 1,
  SavingComments: 2,
}
// all property value should be number
 
/** @enum {function(number): number} */
const Math = {
  add1: n => n + 1,
  id: n => -n,
  sub1: n => n - 1,
}
// all property value should match the function signature

内联object

1
2
/** @type {{ foo1: string, foo2: number }} */
let foo;// foo will be declared as an object, foo.foo1 must be a string, foo.foo2 must be a number.

type checking React

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
// stateless component, cause it's just a function, we can declare the props as parameter directly.

/** @param {{className: string, content: string}} props */

const Button = props => (
  <button class={props.className}>{props.content}</button>
);


// stateful component. It's just a class, so we treat it as a class

/** 
 * @extends {Component<{className: string, content: string)>} 
 */

class Button extends Component{
// ...
}

三方类型

三方类型在这里既可以是 npm提供的 @types包,也可以是自己在项目中通过TypeScript定义的类型。 只要在typeRoots这个编译器选项中指明即可。

如何和项目做集成

IDE和编辑器集成

项目以使用vscode和webstorm为主,但是理论上,只要能支持TypeScript Language Server Protocal的编辑器都不会出现集成上的问题。

vscode

对于vscode,不需要做过多的配置,直接在文件头部加入 // @ts-check的comment就可以开启TypeScript checking.

WebStorm

对于WebStorm,你需要提供对应的tsconfig.json文件来显式的打开这个特性。

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
{
    "compilerOptions": {
        "allowJs": "true", // 开启js checking
        "noEmit": "true",  // 不生成输出文件
        "target": "es6",    // 支持es6语法
        "checkJs": "true"  // 打开会检查所有的js, 如果不需要可以手动的在需要检查的js文件头部加上 @ts-check的标记.
    },
    "include": [
        "src/**/*"
    ],
    "exclude": [
        "node_modules",
        "**/*.spec.ts"
    ]
}

工作流集成

除开IDE和编辑器等开发工具的集成之外,我们还需要把TypeScript Checking JavaScript集成进我们的构建流程之中。TypeScript的CLI提供了非常强大的功能,由于在tsconfig.json中声名了只做类型检查,不做实际的编译,所以通过一条简单的tsc命令就能很好的帮助我们在构建中检查我们的代码是否存在构建错误。

一些限制

凡事都有两面性,Type Checking JS也并非没有缺点。在一段时间的试验后,我发现和直接编写TypeScript相比,Type Checking JS存在以下不爽的地方

1. 使用comments破坏代码语义化

比起类型声明和代码结合在一起,使用comments的形式难免在阅读和编写的时候感觉有些别扭,在代码上也有一定的冗余。

1
2
3
4
5
6
// ts 写法
let foo:string = (bar: string) => `${bar}`

// type checking js写法
/** @type {(bar: string) => string}
let foo = bar => `${bar}`

2. 语法支持不统一

Webstorm出于某种原因并不支持typescript like的语法,我们只能退而求其次选择closure like的语法。 这让Type Checking JS的写法下降了不止一个档次。

总结

虽然最后聊到了Type Checking JS的一些缺陷和限制,但是这并不妨碍其成为改造老旧项目或者向TypeScript过渡的比较不错的一个方案。最后也希望社区能继续推动和改进这种写法,让更多的老旧项目尽可能多的享受TypeScript带来的各种便利的特性。

参考

Type Checking JavaScript Files · TypeScript