评论

都 9102 年了,TypeScript 了解一下

TypeScript 是由微软开发并开源的一门编程语言,其作为 Javascript 的超集被广大 Web 前端所熟知,让我们一起走近 TypeScript,了解这门越来越火的编程语言。

前言

此文章为 SlugTeam 大前端技术沙龙 TypeScript 主题分享 PPT 文字内容。SlugTeam 大前端是腾讯互娱市场平台部营销技术中心下属几个开发组前端技术人员的联合,自去年 9 月起,SlugTeam 大前端每 3 个星期举行 1 次技术沙龙,沙龙主题由团队成员推荐并投票选出。

本次 3 月 28 日沙龙主题为 TypeScript。TypeScript 是由微软开发并开源的一门编程语言,其作为 Javascript 的超集被广大 Web 前端所熟知,即使在实际工作中尚未使用,也一定听说过 TypeScript。

接下来,让我们一起走近 TypeScript,了解这门越来越火的编程语言。

1. TypeScript 简介

1.1 JavaScript 的诞生

1995 年,网景公司发布了一门叫做 Javascript 的脚本语言,它由当时 34 岁的系统程序员 Brendan Eich 设计,仅仅用于在浏览器实现一些简单的网页互动,如表单验证。

Brendan Eich 做梦也没想到,自己花了十天仓促设计出来的 JavaScript,一经推出就被广泛接受,获得了全世界范围内大量的用户使用。JavaScript 这样连续的爆发式扩散增长,使得语言规格调整困难重重。

1996 年,微软公司也推出了自己的脚本 JScript。为了压制微软,网景公司决定申请 JavaScript 国际标准。

1997 年 6 月,第一个国际标准 ECMA-262 正式颁布。

从推出到颁布标准,JavaScript 仅用了一年半时间,语言的缺陷没有充分暴露,就已经被标准固化下来。

1.2 AJAX 的流行

在 2005 年以前,Web 应用程序开发还停留在完成简单的交互逻辑,受限于浏览器技术,Web 应用程序的交互行为十分单调,缺失像桌面应用那样丰富的交互,每次用户与服务器交互都需要重新刷新页面才能够看到效果。

2005 年初,Google 在其著名的 Web 交互应用程序中大量使用了 AJAX,如 Google、Google Map、Gmail等,无需刷新页面就能实现页面的异步更新。Google 对 AJAX 技术的实践,迅速提高了人们使用该项技术的意识。而使用了 AJAX 技术的 Web 应用程序,可以模拟出传统桌面应用的使用体验。

从此,Web 应用程序逐渐变得复杂而丰富,出现了越来越多的大型 Web 应用程序。

1.3 TypeScript 的由来

正如上面介绍的那样,JavaScript 设计之初就没考虑过可以用来编写大型应用。

JavaScript 诞生之初时的 Web 应用程序也就只有几十行或者几百行代码。到了 2010 年,Web 应用程序的代码已经达到了成千上万行。

2010 年的 JavaScript 标准为 ECMAScript 5.0 版本,尚未拥有类、模块等适合大型应用开发的概念。

与此同时 JavaScript 是动态脚本语言,这意味着没有静态类型,使得 IDE 无法提供诸如“代码补全”、“IDE 重构”(借助于 IDE 来对代码进行重构)、“属性查询”、“跳转到函数定义”等强大功能。

再加上 JavaScript 本身语言上的缺陷,使用 JavaScript 编写大型应用成了一件非常艰巨的任务。

微软卓越的工程师 Anders Hejlsberg(Delphi 和 C# 之父),留意到过去五年听到越来越多开发者吐槽 “JavaScript 难以编写大型应用程序”,使得他思考起未来 JavaScript 将何去何从。

Anders Hejlsberg 发现,不少开发者为了编写大型 Web 应用程序,会选择使用 GWT(Google Web Toolkit, 将 Java 代码编译成 JavaScript) 或者 Script# (将 C# 代码编译成 JavaScript),这样他们就可以借助 eclipse 这种强大的 IDE 辅助开发。

但是这种模式有着很大的缺点:在浏览器上面调试一堆由编译器生成的 JavaScript 是件很让人崩溃的事情。而且程序员写的是 Java,出来的是 JavaScript,这本身就是一件很违和的事情。

于是,Anders Hejlsberg 把心思放在了增强 JavaScript 能力上面,思考如何在提供诸如类、模块、可选的静态类型等概念的同时,又不牺牲 JavaScript 现有的优点。

2012 年 10 月,Anders Hejlsberg 带领团队开发出了首个公开版本的 TypeScript。

2 TypeScript 知识

按照官方的说法:

(1) TypeScript 设计目标是开发大型应用

(2) TypeScript 是 JavaScript 的严格超集,任何现有的 JavaScript 程序都是合法的 TypeScript 程序,包括各种 JavaScript 库

(3) TypeScript 增加了静态类型、类、模块、接口和类型注解

接下来,我们将通过一些实际例子,来学习了解 TypeScript (非教程,若想深入学习请到官网)。

2.1静态类型批注
/**
 * @param (string) x
 */
function process(x){
	x.name = 'foo';
	var v = x + x;
	alert(v);
}

以上是一段日常很常见的 JavaScript 代码,定义了一个名为 process 的函数,传入参数 x,并利用工具生成参数注释。

而在 TypeScript 中,我们可以选择给参数添加类型批注:

function process(x:string){
	x.name = 'foo';
	var v = x + x;
	alert(v);
}

加了类型批注后,编译时候就会启动类型检查:“name” 下面出现了红色的错误提示波浪线,将鼠标移动至上面,则提示 “Property ‘name’ does not exits on type ‘string’.”。

继续将鼠标移动到第二行的 “v” 上面,提示 “(local var) v:string”,TypeScript 根据 x 的类型批注,很智能地推断出了 v 的类型。

function process(x: boolean){
	var v = x + x
}

我们试着把 x 标注为 boolean 类型,修改后 ‘x + x’ 下面会出现错误提示波浪线,提示 “Operator ‘+’ cannot be applied to types ‘boolean’ and ‘boolean’.”。

function process(x:string[]){
    x[0].
}

我们试着将 x 类型批注改为字符串数组,输入 ‘x[0].’ 后,编译器会显示对应的智能提示。

该功能十分强大,它使得开发者不需要记忆过多的 api 或 property,直接就能在右侧找到目标对应的 api 或者 property。

开发者也无需担心这些 api 或 property 会出现张冠李戴的问题,能展示出来的方法或者属性在语法上都是正确的。

像 sublime text 之类的编辑器插件,虽然也能够提供代码补全,但是有可能补全的 api 或者 property 是错误的,因为它们的原理是根据输入的字符串做一些简单的匹配。

例如输入 ‘document’,接着输入’.g’,也即是 ‘document.g’,插件匹配到该字符串后,代码补全提示仅仅显示 ‘getElementsByTagName’ 方法。

这在 TypeScript 未出现之前,的确为开发提供了很大的便利。但是和 TypeScript 的代码补全对比起来,还是显得不够智能。

传统的参数注释,遇上结构化的对象参数,就很束手无策了。而在 TypeScript 中,我们可以标注任何类型的参数:

interface Thing{
    a: number;
    b: string;
    c?: boolean;
}

function process(x: Thing){
    return x.c;
}

var n = process({a:10, b:"hello"});

上面代码中,在参数后面添加 ‘?’,表示参数可选。

interface Thing{
    a: number;
    b: string;
    foo(s: string): string;
    foo(n: number): number;
}

function process(x: Thing){
    return x.foo("12");
    //return x.foo(2);
}

此外,TypeScript 还支持方法重载。

上面的 foo 方法也可以写成下面的格式:

interface Thing{
    a: number;
    b: string;
    foo: {
        (s: string): string;
        (n: number): number;
        data: any;
    };
}

function process(x: Thing){
    return x.foo.data;
    //return x.foo(2);
}

对于函数,TypeScript 也能够批注其为某个接口的实现。

interface Accumulator {
    clear(): void;
    add(x:number): void;
    result(): number;
}
function makeAccumulator(): Accumulator{
    var sum = 0;
    return {
        clear: function() {sum = 0},
        addx: function(value: number){sum += value},
        result: function(){return sum}
    }
}

var a = makeAccumulator();
a.add(5);

makeAccumulator 里面的 ‘addx: function(value: number){sum += value}’ 将会被标红,因为 Accumulator 接口并没有定义 addx 函数。

window.onmousemove = function(e){return e.clientX};
var hash = location.hash;

TypeScript 内部维护了一份 DOM/BOM 的方法属性,省去了用户自己添加对应批注的功夫。例如将鼠标移动到 ‘hash’ 上面,会提示 ‘var hash: string’,TypeScript 就能自动推断出类型。

TypeScript 最终将会编译成 JavaScript,在 TypeScript 上面定义的类型,编译后实际上是不存在的。

addingTypes.ts

function Greeter(greeting: string) {
    this.greeting = greeting;
}

Greeter.prototype.greet = function() {
    return "Hello, " + this.greeting;
}

let greeter = new Greeter("world");

let button = document.createElement('button');
button.textContent = "Say Hello";
button.onclick = function() {
    alert(greeter.greet());
};

document.body.appendChild(button);

上面 TypeScript 文件编译为 JavaScript 后,将是下面的样子:

addingTypes.js

function Greeter(greeting) {
    this.greeting = greeting;
}
Greeter.prototype.greet = function () {
    return "Hello, " + this.greeting;
};
var greeter = new Greeter("world");
var button = document.createElement('button');
button.textContent = "Say Hello";
button.onclick = function () {
    alert(greeter.greet());
};
document.body.appendChild(button);

2.2 类
class Point{
    x: number;
    y: number;
    private color: string;
    constructor(x: number, y: number){
        this.x = x;
        this.y = y;
        this.color = "red";
    }
    dist(){ return Math.sqrt(this.x * this.x +this.y * this.y);}
    static origin = new Point(0,0);
}

var p = new Point(10, 20);
p.x = 10;
p.y = 20;
//p.color = "green"

在 TypeScript 的类中,访问修饰符 public、private、protected、static 和 JAVA、C# 等语言类似。

class Point{
   private color: string;
   constructor(public x: number = 0, public y: number = 0){
       this.color = "red";
   }
   dist(){return Math.sqrt(this.x * this.x + this.y * this.y)}
   static origin = new Point(0,0);
}

var p = new Point();
p.x = 10;
p.y = 20;

TypeScript 支持默认值,用法和 ES6 类似。

接下来,我们看一下 TypeScript 如何实现类的继承。

class Point{
   private color: string;
   constructor(public x: number = 0, public y: number = 0){
       this.color = "red";
   }
   dist(){return Math.sqrt(this.x * this.x + this.y * this.y)}
   static origin = new Point(0,0);
}

class Point3D extends Point{
    constructor(x: number, y: number, public z:number){
        super(x,y);
    }
    dist() {
        var d = super.dist();
        return Math.sqrt(d * d + this.z * this.z)
    }
}

让我们继续看下一段代码

class Tracker{
    count = 0;
    start(){
        window.onmousemove = function(e){
            this.count++;
            console.log(this.count);
        }
    }
}

var t = new Tracker();
t.start();

这段代码在 ‘count++’ 处有个错误提示, “Property ‘count’ does not exist on type ‘GlobalEventHandlers’”,这也是新手在写 JavaScript 时候容易犯的一个错误,而且还不容易察觉。

我们可以使用箭头函数来修复这错误。

class Tracker{
    count = 0;
    start(){
        window.onmousemove = e => {
            this.count++;
            console.log(this.count);
        }
    }
}

var t = new Tracker();
t.start();
2.3 模块
module Utils{
    export class Tracker{
        count = 0;
        start(){
            window.onmousemove = e => {
                console.log(this.count)
            }
        }
    }
}

module Utils {
    export var greeting = "hello"
}

var t = new Utils.Tracker();
t.start;

假如模块带有很长的命名空间,如下面的代码

module Acme.Core.Utils{
    export class Tracker{
        count = 0;
        start(){
            window.onmousemove = e => {
                console.log(this.count)
            }
        }
    }
}


var t = new Acme.Core.Utils.Tracker();
t.start;

我们可以 import 的形式来缩短命名空间。

module Acme.Core.Utils{
    export class Tracker{
        count = 0;
        start(){
            window.onmousemove = e => {
                console.log(this.count)
            }
        }
    }
}

import ACM = Acme.Core.Utils;
var t = new ACM.Tracker();
t.start;

假如我们要使用诸如 Nodejs 的模块,我们要先安装对应的 @types 文件,基本上流行的库都有社区维护的 type 文件。

网站 https://microsoft.github.io/TypeSearch/ 可以查询相关库的 type 文件。

接下来拿 Nodejs 做个演示。

yarn global add typescript
cd typescript_demo
tsc --init
yarn add @types/node

在目录打开 tsconfig.json 文件,修改对应字段

"typeRoots": [
    "./node_modules/@types"
 ], 
"esModuleInterop": true 

创建 server.ts 和 hello.ts,代码分别如下

server.ts

import * as http from 'http'
//import http from 'http'
//上面写法会报“TS1192: Module '"http"' has no default export”
export function simpleServer(port: number, message: string){
    http.createServer((req, res) => {
        res.writeHead(200, {'Content-Type': 'text/html'});
        res.write(`<h1>${message}</h1>`);
        res.end()
    }).listen(port)
}
hello.ts

import {simpleServer} from './server';
simpleServer(1337, "Greetings Channel 9");
console.log("Listening...");

保存后,编译 hello.ts 并运行 hello.js。

tsc hello.ts
node hello

在浏览器访问 localhost:1337 可看到页面输出 Greetings Channel 9

2.4 Visual Studio Code 与 TypeScript

Visual Studio Code 是前端界最流行的 IDE,由微软开发并开源。

Visual Studio Code 加上 TypeScript,代码重构将变得很轻松。

我们在进行代码重构的时候,常常会遇上这么一种情况:修改了方法名或者属性名,还得找到引用它们的地方,逐个修改。假如引用的地方很多,修改将很耗费时间,也很繁琐,而且还可能修改得不够彻底,造成程序错误。

有了 TypeScript 后,在 Visual Studio Code 上,只需要在方法或者属性定义处,右键选择“重命名符号”,输入新的名字,回车后所有的引用将自动更新该变化。

2.5 TypeScript 与热门前端框架

前端目前流行三大框架: Angular、React、Vue。

其中 Angular 基于 TypeScript 来开发,React 在开发的时候可以选择引入 TypeScript,而对于 Vue,作者是这么说的:

必须要承认的是,2.x 的 TS 支持显然跟 React 和 Angular 是有差距的,这也是为什么 3.0 要加强这一块

这里推荐看一下尤雨溪在知乎上面的回答:TypeScript 不适合在 vue 业务开发中使用吗?

虽然目前 Vue 对 TypeScript 的支持不是很完美,但是大部分 TypeScript 的功能还是可以用,应用到生产环境中是个不错的选择。

Vue CLI3 已经支持生成 TypeScript 项目,假如是使用 React 的话,可以选择用 create-react-app 创建项目。

3. TypeScript 相关

3.1 TypeScript 与 ESLint

在使用 TypeScript 的时候,一般也会加入 TSLint,TSLint 事实上已经是 TypeScript 项目的标准静态代码分析工具。TSLint 的生态由一个核心的规范集,社区维护的多种自定义规则以及配置包组成。

ESLint 是 JavaScript 的标准静态代码分析工具。相对于 TSLint,ESLint 支持 TSLint 所缺少的很多功能,如条件 lint 配置和自动缩进。

有一段时间,TypeScript 的代码检查主要有两个方案:使用 TSLint 或使用 ESLint + typescript-eslint-parser。

在 2019 年年初,由于效能问题, TypeScript 官方决定全面采用 ESLint。

接下来 ESLint 团队将不再继续维护 typescript-eslint-parser,他们会封存储存库,也不会在 Npm 发布 typescript-eslint-parser,原本使用 typescript-eslint-parser 的开发者应使用 typescript-eslint/ parser 替代

3.2 Deno

“Node 现在太难用了!”,Nodejs之父 Ryan Dahl 去年年初要开发一款 JavaScript 数据互动分析工具的时候,忍不住抱怨自己十年前创造的技术。

尽管 Nodejs 大受欢迎,但是 Ryan Dahl 在 2018 年的演讲时,坦言 Nodejs 有十大设计错误。

Ryan Dahl 决定偿还当年的技术债,打造一个全新的服务端 JavaScript 运行环境,也就是 Deno 项目。

Deno 跟 Nodejs 一样采用了 V8 引擎,但 Deno 是以 TypeScript 为基础,提高了代码的准确性,并将编译器内置到 Deno 可执行文件中。

需要一提的是,Deno 项目现在属于飞速发展的阶段,源码随时可能更新。

4 总结

4.1 TypeScript 优点
  1. 解决了 IDE 无法智能提示的问题
  2. 函数文档化,无需看接口文档即可直观了解函数参数及对应类型
  3. 类型检查以及错误提示
  4. 放心地进行代码重构
  5. 提供了业界认可的类、泛型、封装、接口面向对象设计能力,以提升 JavaScript 的面向对象设计能力
4.2 TypeScript 缺点
  1. npm 绝大多数模块没有类型注解,假如在 @types 里面也没有找到对应的类型定义文件 (*.d.ts),需要自己手写一份
  2. 额外的语法学习成本
  3. TypeScript 配合 webpack 或者 babel 等工具时需要额外处理一些异常

5 后记

在沙龙最后的讨论阶段,SlugTeam 成员一致认为 TypeScript 代表了前端未来发展的方向,项目迁移成本不算高,可以逐步推广起来。

还没了解 TypeScript 的同学,强烈安利:)

最后一次编辑于  2019-04-17  
点赞 12
收藏
评论

4 个评论

  • 陈式坚
    陈式坚
    2019-04-22

    仿佛来到了古代掘金....

    2019-04-22
    赞同 1
    回复
  • 我坚信能看到彩虹尽管依然在风雨中
    我坚信能看到彩虹尽管依然在风雨中
    2019-04-19

    好顶赞

    2019-04-19
    赞同 1
    回复
  • 2019-11-08

    赞一个,哈哈哈

    2019-11-08
    赞同
    回复
  • 2019-04-19

    有毛用,小程序官方的 typings 做了一半就弃坑了……

    2019-04-19
    赞同
    回复
登录 后发表内容