TypeScript Sucks but Long Live the Types

查看PPT

Part I : Type System

JavaScript 是一种很爽的语言

作为一款动态语言,比如你可以执行以下代码而不用担心报错。

var num = "3" - "2"; // num = 1

但是,如果遇到了下面这种情况,就算是灾难了。

isGreater("1000", "233"); // false

function isGreater(, b) {
    return a > b;
}

所以我们写代码的时候,都会加注释

/**
* 是否为最大
* 参数传数字啊
* 谁不传谁是小狗
* @constructor
* @param {number}
* @param {number} b
* @returns {boolean}
**/
function isGreater(, b) {
    return a > b;
}

可是总有人不看注释,所以这个函数变成了

function isGreater(a, b) {
    const T_NUM = "number";
    if (
        T_NUM === typeof a &&
        T_NUM === typeof b
    ) {
        return a > b;
    } else {
        throw("参数传数字啊,扎心了,老铁!");
    }
}

这还没完呢,假设这次改动出现在某次重构中,此前这个isGreater方法已经遍布你的项目了。

btn.onclick = () =>
    isGreater("x", "y") ? ... : ...;

而正好,这个 onClick 在自测的时候被忽略了。上线之后就是灾难了。


动态一时爽,重构火葬场

举个 TS 的栗子

function isGreater(: number, b: number): boolean {
    return a > b;
}

类型系统的优势

比较流行的JS类型系统

Part II : TS vs Flow

众多类型系统里面,typescript 和 flowtyped 的比较类似,目标都是为了 JS 的工程化,而且语法上除了细节几处根本没区别。

对照

TypeScriptFlow-typed
byMicrosoftFacebook
inTypeScriptOCamel
is ACompilerChecker
VSCode原生支持安装插件
webpackts-loaderbabel
configjsonconfig
version2.3.20.54.1

简单比对

TypeScript

function isGreater(a: number, b: number): boolean {
    return a > b;
}

isGreater("1", "2");
      // ^^^^
      // Argument of type "1" is not assignable to parameter of type 'number'.

Flow-typed

//@flow
function isGreater(a: number, b: number): boolean {
    return a > b;
}

isGreater("1", "2");
        // ^^^  ^^^
        // string (This type is incompatible with the expected param type of number)

空值检查

TypeScript(strict) 和 Flow 都能指出 nullCheck 函数应该指明返回值为 string | void 类型。

function nullCheck(num: number): string {
                                // ^^^
                                // [ts] Function lacks ending return statement and return type does not include 'undefined'.
                                // [flow] This type is incompatible with an implicitly-returned undefined.
    if (num > 10) {
        return "it's ok"
    }
}

泛型

TypeScript 和 Flow 都指出狗的数组不能加入猫的实例。

class Animal { }
class Dog extends Animal { woff = true }
class Cat extends Animal { meow = true }

let animals: Animal[] = [];
let dogs: Dog[] = [];
let cat: Cat[] = [];

animals.push(new Animal);
animals.push(new Dog);
animals.push(new Cat);

dogs.push(new Cat);
      // ^^^^^^^^
      // 'Argument of type 'Cat' is not assignable to parameter of type 'Dog'.
      // ^^^^^^^ Cat. This type is incompatible with

此处 TS 和 Flow 都能查出错误,但是报错位置不同。

//@flow
class Animal {}
class Dog extends Animal { woff = true }
class Cat extends Animal { meow = true }

let animals: Animal[] = [];
            // ^^^^^^
            // [flow] Animal (This type is incompatible with Cat)'
let cats: Cat[] = animals;
// ^^^
// [ts] Type 'Animal[]' is not assignable to type 'Cat[]'.

Wait for IT

震惊,TypeScript 在这种情况下不报错!!!

//@flow
class Animal {}
class Dog extends Animal { woff = true }
class Cat extends Animal { meow = true }

let cats: Cat[] = [];
let animals: Animal[] = cat;
        // ^^^^^^^^^
        // [flow] Animal (This type is incompatible with Cat)

animals.push(new Dog);
animals.push(new Cat);
animals.push(new Animal);

JSON.stringify(cats); // [{"woff":true},{"meow":true},{}]

TS 只做了类型检查,而 JS 数组是引用赋值的,因此引起了错误

Nominal & Structural (property-based) Typing

Flow 是标称类型而 TS 是结构类型。

class Animal { }
class Dog extends Animal { name = "dog" }
class Cat extends Animal { name = "cat" }

let dog: Dog = new Dog;
dog = new Cat;
      // ^^^^^^^ [Flow] Cat. This type is incompatible with Dog
dog = { name: "dog" };
      // ^^^^^^^^^^^^^^^ [Flow] object literal. This type is incompatible with

对比总结

 TypeScriptFlow-typed
工具支持 有亲爸做编辑器 目前体验存在迟钝,但可以做 linting 前的最后工序
第三方库定义支持 npm @types 私有库 flow-typed 可以安装定义,但明显少于 TS
质量检查 tslint eslint (jshint 不行)
编译  可以直接编译为各版本的 JS借助babel
迁移成本 可以把原来的 JS 作为类库使用,但整体依然要用 TS 写可以指定要检查的文件
其他 支持很多es7/8的功能 只是检查器,但是类型检查强于 TS

Part III Types in use (TypeScript)

Library

JavaScript 的类库一直是良莠不齐,所以当你有两个以上的类库可选的话,究竟选哪个可能是个问题。

好在如果一个 JS 类库存在类型声明的话,可以说明这两个问题。

那么在使用这些类库的时候起码不会有太多的坑。

NPM declarations

假如你是一个类库的作者,你已经用 typescript 写完了一个库,正准备把它发布到 npm 上面,如果你想给这个类库加上类型声明,只需在 typescript 的编译配置 (tsconfig.json) 上添加如下字段。

"declaration": true

再次编译,你会看到很多*.d.ts文件,这就是类库的声明文件。

function isGreater(: number, b: number): boolean;

接下来,在package.json添加 type 字段,再发布的 npm 包就已经带有声明了。

"types": "./lib/main.d.ts",

另外,通过在 @types 下搜索也能找到第三方的类库声明。

3rd party declaration

有的时候,你的类库并没有声明文件,同时 @types 也没有其他人上传,你需要自己写声明文件。

比如 HySDK, 只需要在项目目录里添加 .d.ts 文件

declare module "@qnpm/hysdk" {
    export = {
        openWebView: (param: {
            url: string;
            name?: string;
            data?: any;
        }) => void,
    }
}

import { openWebView } from "@qnpm/hysdk";
openWebView({ url: "" });

Types in Redux

前端交互逻辑用得最多的就是发布订阅模式了, 在发布订阅模式里面需要一个约定值来确定订阅的是哪一个事件。 那么类型系统就派上用场了,这里拿 Redux 举例。


Redux 的问题在于,订阅和发布的约定值可以随便写。

const store = createStore((state,{type, data}) => {
    if (type === "action1") return ...
    if (type === "action2") return ...
    return state;
})
function doAction(action) {store.dispatch(action);}
doAction({ type: "action1", data }); // OK
doAction({ type: "actionX", data }); // OK

我们给 action 写一个约束 IAction,这样, 在 reducer 里面不能多写一个 actionY 事件, dispatch 的时候也不能指定 actionX 事件。

interface IAction { type: "action1" | "action2", data: any }
const store = createStore((state,{type, data}: IAction) => {
    if (type === "action1") return ...
    if (type === "action2") return ...
    if (type === "actionY") return ... // Err
    return state;
})
function doAction(action: IAction) {store.dispatch(action);}
doAction({ type: "action1", data }); // OK
doAction({ type: "actionX", data }); // Err

Part IV Epilogue

应该使用类型系统吗?

页面只用于接下来的十一活动营销 No
页面很简单就是展示文案No
新项目,并以后很有可能会频繁重构 TypeScript
老项目,老文件不再改,只增加文件Flow

其实,前端的类型系统到目前为止依然还有很多瑕疵,因此我说 TypeScript sucks,但是就目前的版本来说,可比它刚发布那会儿要好用的多了,起码已经到了能用的地步。

随着 TS 和 Flow 的发展和互相影响,前端类型系统将会逐步完善,但那都是以后的事了。就目前来说,如果你手上有一个大工程,还没有用上类型系统,那为何不尝试一下呢?