TypeScript

背景认识:

TypeScript 是微软开发一款开源的编程语言,本质上是向 JavaScript 增加静态类型系统。它是 JavaScript 的超集,所有现有的 JavaScript 都可以不加改变就在其中使用。它是为大型软件开发而设计的,它最终编译产生 JavaScript,所以可以运行在浏览器、Node.js 等等的运行时环境。

静态类型系统是什么

增加静态这个定语,是为了和运行时的类型检查机制加以区分,强调静态类型系统是在编译时进行类型分析。

JavaScript 不是一个静态编译语言,不存在编译这一步骤。但从程序推理工具的角度来看,JavaScript 的配套中还是有不少的,比如ESLint这个不完备的程序推理工具

静态类型系统与 Lint 工具的关系

ESLint的定义:

Code linting is a type of static analysis that is frequently used to find problematic patterns or code that doesn’t adhere to certain style guidelines.

区别一

同样强调Static Analysis,不过更强调Certain Style Guidelines,Lint 工具是一种团队协作时的风格规范工具。

区别二

静态类型类型分析和Lint 工具的区别在于Lint 工具没有Classifying phrases according to the kinds of values they compute。

Lint 工具无法基于类型对程序进行静态分析,但两者都有基于CFG (控制流图,Control Flow Graph)对程序进行分析的能力。比如 TypeScript 的控制流分析、ESLint的complexity(当你想写个比较复杂的迭代算法时,这个规则就是个渣) 规则等。

TypeScript 和 JavaScript 的关系

和一些基于 JavaScript 的激进语言不同(比如 CoffeeScript),TypeScript 的语法设计首先考虑的就是兼容 JavaScript,或者说对 JavaScript 的语法做扩展。基本上是在 JavaScript 的基础之上增加了一些类型标记语法,以实现静态类型分析。把这些类型标注语法去掉之后,仍是一个标准的 JavaScript 语言。

TypeScript 同样也在做一些新语法编译到老语法的事情(就像 Babel 做的), 基本实现常用的EcmaScript Stage 1以上的语法特性。

类型系统的益处

侦测错误

静态类型分析首要优点就是能尽早的发现逻辑错误,而不是上线之后才发现。比如我们在 JavaScript 中经常发生的问题,函数返回值含混。在开发过程中坚信一个函数返回字符串,但到了线上接受了真实数据却返回了undefined。看似一个简单错误,却可能给公司造成数以万计的损失。

看个例子。

// 通过分数获取图标

functiongetRankIcon(score){

if(score >=100) {

return'';

}elseif(score >=500) {

return'';

}elseif(score >=1500) {

return'';

}

}

consticon = getRankIcon(5);

consticonArray = icon.split();

执行

> node taste.js

TypeError: Cannot read property 'split' of undefined

相同的逻辑我们用tsc编译一下(甚至不需要增加任何的类型标注)。直接静态分析出来程序有一个undefined。

> tsc --strictNullChecks taste.ts

x.ts(11,19): error TS2532: Object is possibly 'undefined'.

另一个重要的用处是作为维护工具(重构辅助工具),假如我们有一个很通用的函数,在工程里用的到处都是,有一天我们要在这个函数最前面增加一个参数。TypeScript 中你只需要改那个函数就好了,然后再执行静态类型分析,所有和这个函数参数不匹配的地方都会提示出来。但是,在 JavaScript 里,这个改动很有可能被忽略或者漏掉,打包也不会报错,然后发布后线上就挂了……

抽象

类型系统的另一个优点是强化规范编程,TypeScript 提供了简便的方式定义接口。这一点在大型软件开发时尤为重要,一个系统模块可以抽象的看做一个 TypeScript 定义的接口。

用带清晰接口的模块来结构化大型系统,这是一种更为抽象的设计形式。接口设计(讨论)与最终实现方式无关,对接口思考得越抽象越有利。

换句话说就是让设计脱离实现,最终体现出一种IDL(接口定义语言,Interface Define Language),让程序设计回归本质。

看个例子。

interface Avatar {

cdnUrl: string;// 用户头像在 CDN 上的地址

filePath: string;// 用户头像在对象存储上的路径

fileSize: number;// 文件大小

}

interface UserProfile {

cuid?: string;// 用户识别 ID,可选

avatar?: Avatar;// 用户形象,可选

name: string;// 用户名,必选

gender: string;// 用户性别,必选

age: number;// 用户年龄,必选

}

interface UserModel {

createUser(profile: UserProfile): string;// 创建用户

getUser(cuid: string): UserProfile;// 根据 cuid 获取用户

listFollowers(cuid: string): UserProfile[];// 获取所有关注者

followByCuid(cuid: string, who: string): string;// 关注某人

}

那我实现上述Interface也只需如下进行。

classUserModelImplimplementsUserModel{

createUser(profile: UserProfile): string {

// do something

}

// 把 UserModel 定义的都实现

}

文档

读程序时类型标注也有用处,不止是说人在读的时候。基于类型定义 IDE 可以对我们进行很多辅助,比如找到一个函数所有的使用,编写代码时对参数进行提示等等。

更重要的是这种文档能力不像纯人工维护的注释一样,稍不留神就忘了更新注释,最后注释和程序不一致。

更强大的是,可以自动根据类型标注产生文档,甚至都不需要编写注释(详细的人类语言描述还是要写注释的)。

首先安装全局的typedoc命令。

> npm install -g typedoc

然后我们尝试对上面抽象的Interface产生文档。

> typedoc taste.ts --module commonjs --out doc

然后下面就是效果了。

编写第一个 TypeScript 程序

这一节会介绍如何开始体验 TypeScript,下一节开始会介绍一些有特点、有趣的例子。

前置准备

安装 TypeScript。

npm install -g typescript

初始化工作区。

mkdir learning-typescript

cd learning-typescript

新建第一个测试文件。

touch taste.ts

第一个例子

我们刚才已经新建了一个名为taste.ts的文件,对 TypeScript 的后缀名为ts,那我们写点什么进去吧!

taste.ts

functionsay(text: string){

console.log(text);

}

say('hello!');

然后执行命令(tsc 是刚才 npm 装的 typescript 中带的)。

tsc taste.ts

然后我们得到一个编译后的文件taste.js,内容如下。

functionsay(text){

console.log(text);

}

say('hello!');

可以看到,只是简单去除了 text 后面的类型标注,然后我们用node执行taste.js。

node taste.js

// hello!

完美执行,让我再改写东西看看?

taste.ts

functionsay(text: string){

console.log(text);

}

say(969);

然后再执行tsc taste.ts,然后就类型检查就报错了。这就是 TypeScript 的主要功能 —— 静态类型检查。

> tsc taste.ts

taste.ts(4,5): error TS2345: Argument of type '969' is not assignable to parameter of type 'string'.

有趣的例子 - 基于控制流的分析

看一个 JavaScript 的例子。

functiongetDefaultValue(key, emphasis){

letret;

if(key ==='name') {

ret ='GuangWong';

}elseif(key==='gender') {

ret ='Man';

}elseif(key ==='age') {

ret =23;

}else{

thrownewError('Unkown key '+ info.type);

}

if(emphasis) {

ret = ret.toUpperCase();

}

returnret;

}

getDefaultValue('name');// GuangWong

getDefaultValue('gender',true)// MAN

getDefaultValue('age',true)// Error: toUpperCase is not a function

这是一个简单的函数,第一个参数key用来获得一个默认值。第二参数emphasis为了某些场景下要大写强调,只需要传入true即可自动将结果转成大写。

但是我不小心将age的值写成了数字字面量,如果我调用getDefaultValue('age', true)就会在运行时报错。这个有可能是软件上线了之后才发生,直接导致业务不可用。

TypeScript 就能避免这类问题,我们只需要进行一个简单的标注。

functiongetDefaultValue(key, emphasis?){

letret: string;

if(key ==='name') {

ret ='GuangWong';

}elseif(key ==='gender') {

ret ='Man';

}elseif(key ==='age') {

ret =23;

}else{

thrownewError('Unkown key '+ key);

}

if(emphasis) {

ret = ret.toUpperCase();

}

returnret;

}

getDefaultValue('name');// GuangWong

getDefaultValue('gender',true)// MAN

getDefaultValue('age',true)// Error: toUpperCase is not a function

在tsc编译时,逻辑错误会自动报出来。妈妈再也不怕我的逻辑混乱了!

> tsc taste.ts

x.ts(8,5): error TS2322: Type '23' is not assignable to type 'string'.

有趣的例子 - Interface

JavaScript 的类型我们称为鸭子类型。

当看到一只鸟走起来像鸭子、游泳起来像鸭子、叫起来也像鸭子,那么这只鸟就可以被称为鸭子。

鸭子类型总是有点损的感觉,不如叫做面向接口编程。所以 JavaScript 就是一门面向接口编程的语言,TypeScript 中相对应的就是Interface。

接下来看个例子。

interface Profile {

name: string;

gender:'man'|'woman';

age: number;

height?: number;

}

functionprintProfile(profile: Profile){

console.log('name', profile.name);

console.log('gender', profile. gender);

console.log('age', profile.age);

if(profile.height) {

console.log('height', profile.height);

}

}

printProfile({name:'GuangWong', gender:'man', age:23});

使用tsc编译一切完美,那我们尝试下面的调用。

printProfile({name:'GuangWong', age:23});

使用tsc编译,报错了!说没有传属性gender。不过height也没传怎么没报错呢?因为height?: number,其中的?表示这个是可选的。

> tsc taste.ts

x.ts(19,14): error TS2345: Argument of type '{ name: string; age: number; }' is not assignable to parameter of type 'Profile'.

Property 'gender' is missing in type '{ name: string; age: number; }'.

接下来我们试着传个非number的height试试看。

printProfile({height:'190cm', name:'GuangWong', gender:'man', age:23});

使用tsc编译,报错了!string类型无法赋值给number类型。

> tsc taste.ts

x.ts(17,14): error TS2345: Argument of type '{ height: string; name: string; gender: "man"; age: number; }' is not assignable to parameter of type 'Profile'.

Types of property 'height' are incompatible.

Type 'string' is not assignable to type 'number'.

有趣的例子 - Implements

这也是Interface的应用,假设我们有这么一个Interface,是某个架构师写的让我来实现一种事物,比如榴莲。

type Fell ='good'|'bad';

interface Eatable {

calorie: number;

looks(): Fell;

taste(): Fell;

flavour(): Fell;

}

我只需要简单的实现Eatable即可,即implements Eatable。

classDurianimplementsEatable{

calorie =1000;

looks(): Fell {

return'good';

}

taste(): Fell {

return'good';

}

flavour(): Fell {

return'bad';

}

}

如果我删掉flavour的实现,那就会报错了!说我错误的实现了Eatable。

> tsc taste.ts

x.ts(8,7): error TS2420: Class 'Durian' incorrectly implements interface 'Eatable'.

Property 'flavour' is missing in type 'Durian'.

有趣的例子 - 函数重载

什么重载啊、多态啊、分派啊,在 JavaScript 里都是不存在的!那都是都是我们 Hacking 出来,Ugly!

TypeScript 对函数重载有一定的支持,不过因为 TypeScript 不扩展 JavaScript 的运行时机制,还是需要我们来处理根据宗量分派的问题(说白了就是运行时类型判断)。

下面是 TypeScript 文档中的一个例子。

letsuits = ["hearts","spades","clubs","diamonds"];

functionpickCard(x: {suit: string; card: number; }[]):number;

functionpickCard(x: number):{suit: string; card: number; };

functionpickCard(x):any{

// Check to see if we're working with an object/array

// if so, they gave us the deck and we'll pick the card

if(typeofx =="object") {

letpickedCard =Math.floor(Math.random() * x.length);

returnpickedCard;

}

// Otherwise just let them pick the card

elseif(typeofx =="number") {

letpickedSuit =Math.floor(x /13);

return{ suit: suits[pickedSuit], card: x %13};

}

}

letmyDeck = [{ suit:"diamonds", card:2}, { suit:"spades", card:10}, { suit:"hearts", card:4}];

letpickedCard1 = myDeck[pickCard(myDeck)];

alert("card: "+ pickedCard1.card +" of "+ pickedCard1.suit);

letpickedCard2 = pickCard(15);

alert("card: "+ pickedCard2.card +" of "+ pickedCard2.suit);

这样至少在函数头的描述上清晰多了,而且函数的各个分派函数的类型定义也可以明确的标记出来了。

最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 206,723评论 6 481
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 88,485评论 2 382
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 152,998评论 0 344
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 55,323评论 1 279
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 64,355评论 5 374
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 49,079评论 1 285
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 38,389评论 3 400
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 37,019评论 0 259
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 43,519评论 1 300
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 35,971评论 2 325
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 38,100评论 1 333
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 33,738评论 4 324
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 39,293评论 3 307
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 30,289评论 0 19
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 31,517评论 1 262
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 45,547评论 2 354
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 42,834评论 2 345

推荐阅读更多精彩内容