我的TypeScript实践经验

引言

本文不会详细去介绍TypeScript的基础语法和用法,对TypeScript还不熟悉的同学可以先走马观花的浏览一下相关的文档:

  1. TypeScript 入门教程
  2. TypeScript Handbook
  3. TypeScript 官方文档

几个关键词:类型声明、类、接口、泛型。

一、启用TypeScript

IDE:

我使用的IDE是VSCode,其内置了TypeScript解释器。其他常用的编辑器都可以通过插件的方式提供对TypeScript的支持。

项目构建:

对于项目,现在大家应该都使用webpack打包,所以安装一下必要的依赖:

nenpm install --save-dev typescript awesome-typescript-loader source-map-loader

然后在webpack添加一些配置项,关键部分:

resolve: {
    extensions: [".ts", ".tsx", ".js", ".json"]
},
module : {
    rules : [{   
            test: /\.tsx?$/,
            loader: "awesome-typescript-loader" 
        }, {
            enforce: "pre", 
            test: /\.js$/,
            loader: "source-map-loader"
        },
    ]
}

纯使用TypeScript的话,就没有必要再去使用Babel了。

TSLint:

大部分编辑器可以安装tslint的插件,这是一个和eslint类似并且可以结合TypeScript语法规则进行更有效的规范约定的工具。

通常可以在tslint中继承标准规范:

{ 
    "extends": "tslint:recommended",
    "rules": { 
        /* 你的自定义规则 */ 
    }
}

然后结合具体开发习惯进行规则的修改。

TSLint的全部规则可以在这里查阅到

二、安装第三方类库

常规情况下,我们如果想安装第三方类库(比如lodash)的时候,通常直接如下一个命令即可:

nenpm i lodash --save

但是默认情况下,第三方类库都不会提供.d.ts文件来声明这个模块的属性方法,也就是说我们无法在编辑器中享受到TypeScript带来的类型检查。
安装声明文件的方式有tsdtypings@types,其中前两种已经不推荐了,这里直接使用@types方式安装。
只需变更两个地方: 在原来安装的包名前加上@types/ 。并且换成--save-dev即可。

nenpm i @types/lodash --save-dev

通常绝大部分流行的第三方库都提供了声明文件,可以在http://npm.hz.netease.com/browse/keyword/@types这里浏览和搜索。

三、结合React使用

第一步

首先需要安装@types文件,通常:

nenpm i react --save
nenpm i react-dom --save
nenpm i @types/react --save-dev
nenpm i @types/react-dom --save-dev

第二步

引入React和React DOM的方式如下:

import * as React from 'react';
import * as ReactDOM from 'react-dom';

为什么要这样引入呢, 因为在@types/react的声明文件中, 使用了如下方式进行模块导出:

export = React

这个是在早期Typescript综合CommonJS和AMD提出的导出模块方式,现在ES6出现之后,大家基本都统一了,不过考虑到历史原因和兼容性,基本上所有类库还是以这样的形式导出。这种在ES6中不算默认导出的,所以不能直接以默认导入的方式引进来。

对于此种导出方式,官方给出的导入方式是:

import React = require('react');

是不是有点不伦不类?所以也可以按照TypeScript提供的ES6语法全部导入到一个对象。

import * as React from 'react';

第三步

泛型可以说是TypeScript一个很大的亮点,它可以为我们定义React Component的State和Props类型。

// TypeScript建议Interface类型名称都以I开头
interface IState {
  name: string;
  age?: number;
}

interface IProps {
  score: number;
}

class Student extends React.Component<IProps, IState> {
    // ......
}

至此,在这个component内部访问props和state的都有了类型检查的保障。

如果不想进行类型检查,可以使用any替换IProps或者IState

另外进行类型强转的时候,请不要使用类似这种形式的,因为会被误认为jsx标签...

备注: React的生命周期访问修饰符请使用public。因为这些钩子函数是被React框架主动调用的,可以类比于Android和iOS开发的生命周期函数。

四、关于 tsconfig.json

TypeScript已经尽力做到和未来的ES规范保持一致,基本上ES有的功能,TS都有,所以可以尽管放心食用。

webpack中使用awesome-typescript-loader之后,整体编译的配置项就交给了tsconfig.json,这个文件需要放在项目的最外层。这里只列举最通用的配置项,详细的可以在官方手册中查询

通常只会用到几个配置主项:

compilerOptions: object // 编译设置,下面会重点说明。
include: string[] // 包含目录,如 "src/**/*" 就是指src目录下所有ts相关文件
exclude: string[] // 不编译目录,也会过滤include中的目录
extends: string // 可以继承别的tsconfig文件,填写继承地址

下面说一下compilerOptions相关的子配置项(希望大家看得懂我的描述方式):

allowJs: boolean = false

allowJs允许在ts中编译引入的js文件,通常建议你一旦启用了ts,可以使用any类型, 但是不要出现任何.js或者.jsx文件,所以此项最好设置为false

baseUrl: string

baseUrl用于处理项目中的非相对路径,比如指定为"baseUrl":"./src/util",在你的任意文件中import Common from 'common',则编译器会优先去查找根目录下的./src/util/common文件。

paths: <custom object>

paths需要搭配baseUrl使用,类似webpack的aliash功能,举例来说,设置了paths: { c: ['common'] }之后,则可以直接使用import Common from 'c'的形式。数组是表示按优先级查找的顺序。

experimentalDecorators: boolean = false

experimentalDecorators允许开启类似ES提案中的装饰器功能,用法几乎一模一样。

jsx: 'preserve' | 'react' = 'preserve'

jsx这个配置项去处理如何解析tsx文件,preserve是只转换成jsx文件,后续操作交给比如babel之类的loader,而react则是直接在tsloader这层完成解析的操作,建议改成react

lib: string[]

lib当我们需要在ts中使用一些高级的API,比如Proxy,Reflect甚至Element.prototype.matches(DOM API),我们需要引入TypeScript的内置库,比如es2015, dom等等。

outDir: string

outDir最终生成代码输出目录

sourceMap: boolean = false

sourceMap是否生成sourceMap,建议开启,方便调试。

target: 'ES3' | 'ES5' | 'ES2016'... = 'ES3'

target生成代码支持的ES版本,通常建议使用ES5以兼容浏览器。

module: 'commonjs' | 'ES6'

module最终模块生成方式,如果target是ES5的话就使用CommonJS。

随手附上一个最基本的tsconfig.json配置:

{
    "compilerOptions": {
        "outDir": "./dist/",
        "sourceMap": true,
        "experimentalDecorators": true,
        "lib": ["es2017", "dom"],
        "module": "commonjs",
        "target": "es5",
        "jsx": "react",
        "baseUrl": "./src/"
    },
    "include": [
        "./src/**/*"
    ]
}

五、一些误区

Q1. TypeScript声明的类型,一定要严格匹配吗?

答: TypeScript的引用类型赋值是向下兼容的,举例来说:

interface IFoo {
    name: string;
}

const bar = { name: 'foo', age: 17 };
const foo: IFoo = bar;

这段代码完全可以通过编译,只要bar对象拥有string类型的属性name即可。
并且,你只能访问foo的name属性,无法访问age属性

不过,你不能直接给foo赋不合法的对象字面值,像这样:
const foo: IFoo = { name: 'foo', age: 19 }
这样是无法通过编译的。

Q2. 是否必须严格定义每一个对象的类型?

答: 我个人感觉是没有必要的,当你不进行类型声明的时候,类型就是any。
如果一定要严格类型检查,可以在tsconfig中开启noImplicitAny

Q3. 如何声明一个可以自定义属性的object?

答:如果你在定义对象的时候,以这种方式定义:
const foo: {} = {};
或者 const foo: object = {};

则你永远也不能往foo里面增添属性,除非转为any,但这就意味着这连一个对象都不是了。

我的解法是在全局定义一个AnyObject类型:
class AnyObject {
    [propName: string]: any; 
}

这样定义自定义对象的时候就可以如下使用(注意要先引入AnyObject):
const myObj: AnyObject = {/* balabala */}

Q4. 我要如何给window添加自定义属性?

答:尽管现在直接给window添加属性已经非常不推荐了,不过还是有可能会有这样的情况。
解决办法其实跟上面非常相似,自定义一个window类型。

class MyWindow extends Window {
    public global: string;
}

然后就可以这样访问了:
const global = (window as MyWindow).global;

Q5. 接口和类的区别是什么?

答: TypeScript有一个很灵活的点就是接口可以继承类,这在某种程度上造成了我的困扰......

类:
类的含义大家都理解,TypeScript新增的访问修饰符,private只在内部访问,protected只允许内部和子类访问,public允许全局访问,没有包访问修饰。
也可以使用abstract来修饰类,抽象类一定至少需要一个抽象方法。

接口:
接口用来描述共性特征,可以定义对象有哪些属性和方法。其实类型声明就是接口类型的声明。
接口没有办法指定默认值。
当然,接口里面是无法实现方法的,并且所有的成员都是public属性。

下面来说一下令人头疼的继承关系:

1. 类继承类,单继承,extends
这个应该很常见,是继承的最基本实现,ES6中就已经支持,不再赘述了。

2. 接口继承接口,单继承,extends
接口之间的继承和类非常相似,所有的成员都会继承过来。

3. 类实现接口,多继承,implements
这个也比较常见,举个例子,
class C implements IA, IB {}
则 C 中一定要定义接口声明的类型和方法,并且修饰符设置为public属性。
特别的,如果IA、IB中有同名属性,则属性类型必须要一致,这个也很合理。

4. 接口继承(?)类,单继承,extends
这种情况非常复杂,因为你还需要考虑到继承完类的接口再被实现的问题。
最后我得出的总结是:
接口继承类只适合于修饰符全为public的类,其他修饰符都会使这个接口变得不纯净,不推荐使用。

尾、到底何时使用TypeScript

我个人认为在项目中启用TypeScript是一件很需要魄力的事情,因为这门语言会对习惯了弱类型的开发者带来很大的冲击。

这里不去比较弱类型和强类型的孰优孰劣,只说引用TypeScript的好处:

  1. 提升使用第三方类库的效率,编译器智能提示入参出参,可以避免一直去查文档。
  2. 在项目中定义关键部分数据的类型检查(比如Redux Store管理的State),开发效率和可维护性大大提升。
  3. 定义请求的参数和返回值类型,避免频繁的去NEI查看返回值类型,同时后续换人维护的话也方便。
  4. 有助于在编译阶段就及早发现错误。
  5. 更好的面向对象支持,比如访问修饰符、getter/setter等。

个人想法,在以下情况下适合用TypeScript开发:

  1. 一个提供给他人使用的第三方类库或者API包
  2. 一个可能会迭代很久的完整的大项目,需要团队成员达成框架和技术的共识。

TypeScript不会像CoffeeScript一样快来快退,它有着非常明显的不可替代性。至于是否在项目中启用TypeScript,现在也没有绝对的答案,毕竟工具的诞生都是为了提高效率。

网易云新用户大礼包:https://www.163yun.com/gift

本文来自网易实践者社区,经作者高臻熙授权发布。