背景认识:
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'.
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'.
这也是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);
这样至少在函数头的描述上清晰多了,而且函数的各个分派函数的类型定义也可以明确的标记出来了。