网上关于Typescript的书实在是太少了,搜来搜去好像就《learning Typescript中文版》一本,果断购买之。读了以后发现,关于Typescript语法的部分其实讲的并不多,更多的是前端项目的架构部分讲解。

暂将本书关于typescript的部分做一些整理。

概念

  • Typescript通过向Javascript增加可选的静态类型声明来把Javscript变成强类型的程序语言
  • 可选的静态类型声明可约束函数,变量,属性等程序实体,这样编译器和相应的开发工具就可以在开发过程中提供更好的正确性验证和智能感知

一. 类型系统

1. 静态类型声明与静态类型推导

1
2
3
4
5
6
7
var counter = 0;	/*没有类型声明,Typescript会根据赋值来推测变量的类型。自动推断出类型是number*/
counter = 'asd'; /*类型变了,会报错*/

let a:any = 0;
a = '你好啊'; /*会报错*/

var count; /*无法推断,类型为any*/

2. 基本类型

基本类型有boolean,number,string,array,void和所有用户自定义的enum类型。所有这些类型在Typescript中 ,都是一个唯一的顶层的Any Type类型的子类型,any关键字代表这种类型。

在Typescript中,我们不能把null或undefined当作类型使用

2.1 Array类型的声明

array类型的声明有两种写法。一是可以在数组元素的类型后面跟着[]来表示包含这种类型元素的数组

1
var list:number[] = [1,2,3];

第二种是使用范型数组类型Array

1
var list:Array<number> = [1,2,3]

如果数组里混合了各种类型

1
2
3
4
5
var list:any[] = [1,'asd',true];
list[1] = 100

//也可以这么写(联合类型)
var list:Array<number|string|boolean> = [1,'asd',true];

2.2 enum类型的声明

enum类型是为了给一个数字集合更友好的命名。enum类型中的成员默认从0开始,但你也可以手动设置成员中的值来更改这种默认行为。

1
2
enum Color {Red,Green,Blue}
var c:Color =Color.Green

会编译为如下代码

1
2
3
4
5
6
7
8
9
var Color;
(function (Color) {
Color[Color["Red"] = 0] = "Red";
Color[Color["Green"] = 1] = "Green";
Color[Color["Blue"] = 2] = "Blue";
})(Color || (Color = {}));
var c = Color.Green;

/*{0: "Red", 1: "Green", 2: "Blue", Red: 0, Green: 1, Blue: 2}*/

2.3 void类型

从某种程度上,any的对立面就是void,即所有类型都不存在的时候。你会在一个函数没有返回值的时候看到它。
1
2
3
function warnUser:void(){
alert('asd');
}

2.4 联合类型

Typescript允许联合类型

1
2
3
4
var path:string[]|string;
path = '/temp'
path = ['/temp','/temp1'];
path = 1 /*错误*/

2.5 类型别名

Typescript允许使用type关键字声明类型别名

1
2
3
type PrimitiveArray = Array<string|number|boolean>;
type myNumber = number;
type CallBack = ()=>void;

2.6 环境声明

环境声明允许在Typescript中创建一个不会被编译到Javascript中的变量。

1
customerConsole.log('asd'); //报错 Can't find name customerConsole
1
2
3
4
5
6
interface ICustomerConsle{
log(arg:string):void
}
declare var customeConsole:ICustomerConsle;

customeConsole.log('asd')

二. 函数

1. 函数的类型

下面是声明函数的参数以及返回值的类型,这种是推荐的写法,比较简洁。

1
2
3
function getName(name:string):string{
return "Hi"+name;
}

而函数本身的类型也是可以定义的。函数类型的定义即()=>string这种

1
2
3
4
var getName :(name:string)=>string;
getName = function(name:string):string{

}

interface也可以修饰函数的类型

1
2
3
4
5
6
7
8
9
10
11
12
interface SearchFunc{
(source:string,subString:string):boolean
}

var mySearch : SearchFunc = function(source:string,subString:string){
let result = source.search(subString);
if(result != -1){
return true;
}else{
return false;
}
}

但是函数的类型声名是可以从被赋值的函数中推断出来的,因此对函数本身使用类型声明不是一个好的实践。

2. 有剩余参数的函数

1
2
3
function add(num1:number,num2:number,num3:number):number{
return num1 + num2 + num3;
}

一个剩余参数必须包含一个数组类型

1
2
3
4
5
6
7
function add(...foo:Array<number>){
let result = 0;
for(var i=0;i<foo.lenth;i++){
result += i;
}
return result;
}

3. 函数重载

  • 实现签名必须兼容所有的重载签名
  • 所有重载签名相互之间必须兼容。如果一个函数试图返回一个数字,而另一个试图返回一个数字,则会返回编译错误。
  • 暂时没有想到应用场景。可以用联合类型来替代。
1
2
3
4
5
6
function test(name:string):string;	//	重载签名
function test(name:boolean):string; // 重载签名

function test(value:string|boolean){ //实现签名
return ''
}

特定重载签名

1
2
3
4
5
6
7
8
9
interface Document{
createElement(tagName:'div'):HTMLDivElement
createElement(tagName:'span'):HTMLSpanElement
createElement(tagName:string):HTMLElement
}
let aaa:Document

let oSpan:HTMLSpanElement = aaa.createElement('span')
let oP:HTMLDivElement = aaa.createElement('span') // 报错

4. 泛型

泛型编程是一种编程语言的风格。它允许程序员使用之后才会定义的类型。

在函数名后面增加了一对角括号(<>)表示这是一个泛型函数

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
//没有泛型的话 下面的getEntities函数就得写两次
function getEntities<T>(
url:string,
cb:(list:T[])=>void
):void{
$.ajax({
url : url,
method : 'GET',
success : function(data){
cd(data.items)
}
})
}

getEntities<User>('/api/users',function(users:User[]){
for(var i=0;i<users.length;i++){
console.log(users[i].name)
}
})
getEntities<Order>('/api/orders',function(orders:Orders[]){
for(var i=0;i<users.length;i++){
console.log(users[i].name)
}
})

三. Typescript 中的面向对象编程

1. SOLID原则

  • 单一职责原则(SRP):软件组件(函数,类,模块)必须专注于单一的任务
  • 开闭原则(OCP): 对拓展开放,对修改封闭
  • 里式替换原则(LSP) : 只要继承的是同一个接口,程序里任何一个类都可以被其他类替换。替换完成后,不需要其他额外的工作,程序就能像原来一样运行。
  • 接口隔离原则(ISP):我们应该将那些非常大的接口(大而全的接口)拆分成一些小而具体的接口(特定客户端接口)。
  • 依赖反转原则(DIP):表明一个方法应该遵从依赖于抽象(接口)而不是一个实例(类)的概念。
1.1 单一职责原则(SRP)原则举例

未遵从,因为增加了一个与Person类无关的validateEmail方法

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
class Person{
public name :string;
public age :number;
public email : string;
constructor(name:string,age:number,email:string){
this.name = name;
this.age = age;
if(this.validateEmail(email)){
this.email = email;
}else{
throw new Error('Invalid Email')
}
}
validateEmail(email:string){
return /^\S+@\S+\.\S+$/.test(email)
}
}

let zhangsan = new Person('张三',13,'fff@fang.com')

遵从

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
class Email{
public email : string;
constructor(email:string){
if(this.validateEmail(email)){
this.email = email;
}else{
throw new Error('Invalid Email')
}

}
validateEmail(email:string){
return /^\S+@\S+\.\S+$/.test(email)
}
}

class Person{
public name :string;
public age :number;
public email : Email;
constructor(name:string,age:number,email:Email){
this.name = name;
this.age = age;
this.email = email;
}
}

let zhangsan = new Person('张三',13,new Email('fff@fang.com'))
1.2 里式替换原则(LSP)
举例
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
//先定义一个接口,作用是将一些对象持久化道某种存贮中
interface PersistanceServiceInterface{
save(entity:any):number
}
//将cookie作为存储介质来实现
class CookiePersistanceService implements PersistanceServiceInterface{
save(entity:any):number{
var id = Math.random();
//cookie存储逻辑
return id
}
}
//将localStorage作为存储介质来实现
class LocalStoragePersistanceService implements PersistanceServiceInterface{
save(entity:any):number{
var id = Math.random();
//localStorage存储逻辑
return id
}
}

class FavouritesController{
private _persistanceService : PersistanceService
constructor(persistanceService:PersistanceService){
this._persistanceService = persistanceServicel
}
public saveAsFavourite(articleId : number){
return this._persistanceService.save(articleId)
}
}

new FavouritesController(new CookiePersistanceService())
new FavouritesController(new LocalStoragePersistanceService())

2. 接口(interface)

接口的作用是抽象。本质就是抽象,让我们在写代码之前对所写的东西有个概念。

在大型软件开发时尤为重要,一个系统模块可以抽象的看做一个 TypeScript 定义的接口。

用带清晰接口的模块来结构化大型系统,这是一种更为抽象的设计形式。接口设计(讨论)与最终实现方式无关,对接口思考得越抽象越有利。换句话说就是让设计脱离实现,最终体现出一种 IDL(接口定义语言,Interface Define Language),让程序设计回归本质。

看个例子。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
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 也只需如下进行。

1
2
3
4
5
6
class UserModelImpl implements UserModel {
createUser(profile: UserProfile): string {
// do something
}
// 把 UserModel 定义的都实现
}
修饰对象
1
2
3
4
5
6
interface Obj{
name : string;
age : number
}

let obj:Obj = {name : 1,age : 2}
修饰函数
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
interface SearchFunc{
(source:string,subString:string):boolean
}

var mySearch : SearchFunc = function(source:string,subString:string){
let result = source.search(subString);
if(result != -1){
return true;
}else{
return false;
}
}

//另一种修饰函数的方法
var myFunc:(a:number)=>string = function(a:number):string{return 'Hello'}
修饰数组
1
2
3
4
5
interface StringArray{
[index:number]:string; //这儿的number是索引类型
}

var myArray:StringArray = ["ime","lily"];
修饰类
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
interface ClockInterface{
currentTime : Date;
setTime(d:Date):string
}

class Clock implements ClockInterface{
currentTime : Date;
setTime(d:Date){
this.currentTime = d;
return 'ready';
}
constructor(h:number,m:number){

}
}
接口继承
1
2
3
4
5
6
7
8
9
10
11
interface Shape{
color :string
}

interface Square extends Shape{
sideLength : number
}

var s = <Square>{};
s.color = "blue";
s.sideLength = 10;

混合类型

1
2
3
4
5
6
7
8
9
10
interface Counter {
interval: number;
reset(): void;
(start: number): string
}

let c:Counter;
c.interval = 123;
c.reset();
c(10)

上面相当于一个函数对象,在上面挂载了自己的方法和属性

3. 继承

继承:可以拓展已有的类。

示例:在table组件中,父类中定义了基本的获取数据等方法,子类中负责自己的渲染逻辑

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
class Table{
constructor(){
this.getData()
}
getData(){
let that = this;
$.ajax({
url : url,
success(){
that.renderTable()
}
})

}
renderTable(){

}
}

class myTable extends Table{
constructor(){
//super的时候就执行了getData,也就执行了renderTable
super()
}
//重写了渲染逻辑
renderTable(){
// super.renderTable() //需要的时候可以调取父类中原来的方法
}
}

4. 泛型类

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
class User{
public name : string;
public password : string;
}
class GenericRespository<T>{
private _url : string;
constructor(url){
this._url = url;
}
public getAsync(){
return new Promise((resolve:(entities:T[])=>void,reject)=>{
$.ajax({
url : this._url,
success:(data)=>{
var list = <T[]>data.items;
resolve(list)
}
})
})
}
}
var userRepository = new GenericRespository<User>("./demo/shared/users.json");
userRepository.then((users:User[])=>{})

举个后端返回接口的例子

简单的:返回值格式是相通的,但Data是不同的

1
2
3
4
5
6
public class PageList<T>{
public int PageIndex{get;set;}
public int PageSize{get;set;}
public int TotalCount{get;set;}
public List<T> Data{get;set;}
}

复杂的

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
public class ExecuteResult{
public int Status{get;set;}
public string Message{get;set;}
//多态
//方法名和类名一样 即为构造函数
public ExecuteResult(int status,string message){
this.Status = status;
this.Message = message;
}
//多态
public ExecuteResult(int status){
this.Status = status;
}
}

//继承上面的类,因此有了status和message
public class ExecuteResult<T>:ExecuteResult{
//返回值的data部分
public T Data{get;set;}
public ExcuteResult(int status,string message){
this.Status = status;
this.Message = message;
}
}

public class PageList<T>{
public int PageIndex{get;set;}
public int PageSize{get;set;}
public int TotalCount{get;set;}
public List<T> Data{get;set;}
}

public class OrderData{
//where是范型约束
//PagedList<T> 因为范型约束为OrderListDTO,所以Data也是OrderListDTO类型
public PagedList<T> GetPagedOrderListDTO<T>(OrderQueryDTO query) where T:OrderListDTO{

}
}


//调用
return new ExcuteResult<PageList<T>>(1,'success')
{Data = new OrderData().GetPagedOrderListDTO<T>(query)} //大括号内容是给Data赋值

5. 泛型约束

5.1 泛型约束是什么

比如在上一章节中,我们想要给实例增加一个isValidate方法,用于验证。但是直接添加这个方法会报错,因为泛型不是具体的类型,不会有isValidate方法的。

1
2
3
4
5
6
7
8
9
10
success:(data)=>{
var list = <T[]>data.items;
let items : <T[]>
for(var i=0;i<list.length;i++){
if(list[i].isValidate()){ //报错

}
}
resolve(list)
}

正确的方法:添加泛型约束(即用interface给实例类型做个约束)

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
interface ValidatableInterface{
isValidate():boolean
}
class User implements ValidatableInterface{
public name : string;
public password : string;
public isValidate():boolean{
return true;
}
}
class GenericRespository<T extends ValidatableInterface>{
private _url : string;
constructor(url){
this._url = url;
}
public getAsync(){
return new Promise((resolve,reject)=>{
$.ajax({
url : this._url,
success:(data)=>{
var list = <T[]>data.items; //有尖括号是强制类型转换
var items:T[];
for(var i=0;i<list.length;i++){
if(list[i].isValidate()){
items.push(list[i])
}
}
resolve(list)
}
})
})
}
}
var userRepository = new GenericRespository<User>("./demo/shared/users.json");
userRepository.getAsync().then((users:User[])=>{})

6. 命名空间(namespace)(也叫做内部模块)

models.ts

1
2
3
4
5
6
7
8
9
10
namespace app{
export namespace models{
export class UserModel{

}
export class TalkModel{

}
}
}

Main.ts

1
2
3
4
/// <reference path="models.ts" />

var user = new app.models.UserModel();
var talk = new app.models.TalkModel();

关于输出:

  1. 运行tsc --output output.js main.ts,会以main.ts为入口,进行打包。打包结果如下:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
var app;
(function (app) {
var models;
(function (models) {
var UserModel = /** @class */ (function () {
function UserModel() {
}
return UserModel;
}());
models.UserModel = UserModel;
var TalkModel = /** @class */ (function () {
function TalkModel() {
}
return TalkModel;
}());
models.TalkModel = TalkModel;
})(models = app.models || (app.models = {}));
})(app || (app = {}));
/// <reference path="models.ts" />
var user = new app.models.UserModel();
var talk = new app.models.TalkModel();
  1. 依次编译,并分别通过script标签引入。

其他问题

一. Typescript中使用async/await

1. 将tsConfig.json中的compilerOptions.lib添加上es2015;
2. 写源代码
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
class Test{
constructor(){
this.goTest()
}

async goTest(){
let aaa = await this.getData();
console.log(aaa);
}

getData(){
return new Promise((resolve,reject)=>{
setTimeout(() => {
resolve(222)
}, 3000);
})

}
}

new Test()
3. 编译
4. 添加promise-polyfill(Typescript会把async/await编译成Promise)

二. jQuery拓展插件声明文件

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
/*这是$.myPlugin静态方法的拓展*/

interface MyPlugin {
settings: MyPluginSettings;

(behavior: 'enable'): JQuery;
(settings?: MyPluginSettings): JQuery;
}

interface MyPluginSettings {
title?: string;
}

interface JQueryStatic {
myPlugin: MyPlugin;
}

$.myPlugin({title:'asd'})

/*这是$().实例方法的拓展*/
interface JQuery{
cxCalendar():void
}
$("#box").cxCalendar()