万字长文,小白必看-TypeScript 学习笔记

1,225 阅读35分钟

TypeScript 学习笔记

每天积累一点,坚持就是收获

image-20210316231748878

一、引言

TypeScript:简称TS是JavaScript的超集

  • TypeScipt = Type + JavaScript (为js添加了类型系统)
  • TypeScript是由微软开发的编程语言,设计目标是为开发大型应用。可以在任何浏览器、任何计算机、任何系统上运行

img

二、安装

2.1 安装TypeScript

1、使用命令安装typescript

cnpm install -g typescript

2、查看typescript版本

$ tsc -v
Version 4.2.2

image-20210304105842023

2.2 安装开发环境

  • vscode:前端开发使用最高的软件之一
  • Live Server:实时服务器
  1. file://由于安全限制,AJAX请求不适用于该协议,即,如果您的站点通过JavaScript获取内容,则您需要一台服务器。
  2. 文件更改后自动重新加载页面可以加快开发速度。

三、基础语法

3.1 引言

TypeScript由以下几部分组成:

  • 模块
  • 函数
  • 变量
  • 语句和表达式
  • 注释

3.2 第一个 TypeScript 程序

1、 新建Runoob.ts文件

var message:string = "Hello World"
console.log(message)

2、 执行编译命令

tsc Runoob.ts

3、 结果

// 编译后得到Runoob.js文件
var message = "Hello World";
console.log(message);

整个流程如下图所:

img

可以编译多个文件:

tsc file1.ts file2.ts file3.ts

3.3 保留的关键字

TypeScript 保留关键字如下表所示:

breakascatchswitch
caseifthrowelse
varnumberstringget
moduletypeinstanceoftypeof
publicprivateenumexport
finallyforwhilevoid
nullsuperthisnew
inreturntruefalse
anyextendsstaticlet
packageimplementsinterfacefunction
newtryyieldconst
continuedo

3.4 注意点

  • 空白和换行

    TypeScript 会忽略程序中出现的空格、制表符和换行符。

    空格、制表符通常用来缩进代码,使代码易于阅读和理解。

  • TypeScript 区分大写和小写字符

  • 分好是可选的

  • 注释

    • 单行注释 ( // ) − 在 // 后面的文字都是注释内容
    • 多行注释 (/* */) − 这种注释可以跨越多行

3.5 TypeScript面向对象基础

TypeScript 是一种面向对象的编程语言:

面向对象主要有两个概念:对象和类

  • 对象:对象是类的一个实例,有状态和行为。例如,一条狗是一个对象,它的状态有:颜色、名字、品种;行为有:摇尾巴、叫、吃等。
  • :类是一个模板,它描述一类对象的行为和状态。
  • 方法:方法是类的操作的实现步骤。

事例:

img

class Site { 
   name():void { 
      console.log("Runoob") 
   } 
} 
var obj = new Site(); 
obj.name();

编译

var Site = /** @class */ (function () {
    function Site() {
    }
    Site.prototype.name = function () {
        console.log("Runoob");
    };
    return Site;
}());
var obj = new Site();
obj.name();

执行js结果

Runoob

四、基础类型

4.1 基础类型表

TypeScript 包含的数据类型如下表:

数据类型关键字描述
任意类型any声明为 any 的变量可以赋予任意类型的值。
数字类型number双精度 64 位浮点值。它可以用来表示整数和分数。image-20210304143318754
字符串类型string一个字符系列,使用单引号(')或双引号(")来表示字符串类型。反引号` 来定义多行文本和内嵌表达式。 image-20210304143208703
布尔类型boolean表示逻辑值:true 和 false。let flag: boolean = true;
数组类型声明变量为数组。image-20210304143358253
元组元组类型用来表示已知元素数量和类型的数组,各元素的类型不必相同,对应位置的类型需要相同。image-20210304143443300
枚举enum枚举类型用于定义数值集合。image-20210304143456903
voidvoid用于标识方法返回值的类型,表示该方法没有返回值。image-20210304143516566
nullnull表示对象值缺失。
undefinedundefined用于初始化变量为一个未定义的值
nevernevernever 是其它类型(包括 null 和 undefined)的子类型,代表从不会出现的值。

**注意:**TypeScript 和 JavaScript 没有整数类型。

4.2 Any类型

在不明确变量的情况下使用的一种数据类型,它常用与一下的三种情况:

  1. 变量值会动态改变时,例如用户输入的值

    let x: any = 4;
    x.ifItExists();    // 正确,ifItExists方法在运行时可能存在,但这里并不会检查
    x.toFixed();    // 正确
    
  2. 改变现有的代码时,意值允许在编译时可选择地包含或移除类型检查,示例代码如下:

    let x: any = 4;
    x.ifItExists();    // 正确,ifItExists方法在运行时可能存在,但这里并不会检查
    x.toFixed();    // 正确
    
  3. 定义存储各种类型数据的数组时,示例代码如下:

    let arrayList: any[] = [1, false, 'fine'];
    arrayList[1] = 100;
    

4.3 Null 和 Undefined

null:

  • 在 JavaScript 中 null 表示 "什么都没有"。

  • null是一个只有一个值的特殊类型。表示一个空对象引用。

  • 用 typeof 检测 null 返回是 object。

undefined:

  • 在 JavaScript 中, undefined 是一个没有设置值的变量。
  • typeof 一个没有值的变量会返回 undefined。
  • Null 和 Undefined 是其他任何类型(包括 void)的子类型,可以赋值给其它类型,如数字类型,此时,赋值后的类型会变成 null 或 undefined。

在TypeScript中启用严格的空校验(--strictNullChecks)特性,就可以使得null 和 undefined 只能被赋值给 void 或本身对应的类型

// 启用 --strictNullChecks
let x: number;
x = 1; // 运行正确
x = undefined;    // 运行错误
x = null;    // 运行错误

上面的例子中变量 x 只能是数字类型。如果一个类型可能出现 null 或 undefined, 可以用 | 来支持多种类型,示例代码如下:

// 启用 --strictNullChecks
let x: number | null | undefined;
x = 1; // 运行正确
x = undefined;    // 运行正确
x = null;    // 运行正确

4.4 never类型

never 是其它类型(包括 null 和 undefined)的子类型,代表从不会出现的值。这意味着声明为 never 类型的变量只能被 never 类型所赋值,在函数中它通常表现为抛出异常或无法执行到终止点(例如无限循环),示例代码如下:

let x: never;
let y: number;

// 运行错误,数字类型不能转为 never 类型
x = 123;

// 运行正确,never 类型可以赋值给 never类型
x = (()=>{ throw new Error('exception')})();

// 运行正确,never 类型可以赋值给 数字类型
y = (()=>{ throw new Error('exception')})();

// 返回值为 never 的函数可以是抛出异常的情况
function error(message: string): never {
    throw new Error(message);
}

// 返回值为 never 的函数可以是无法被执行到的终止点的情况
function loop(): never {
    while (true) {}
}

五、变量声明

5.1 引言

TypeScript变量声明规则:

  • 变量名称可以包含数字和字母。
  • 除了下划线 _ 和美元 $ 符号外,不能包含其他特殊字符,包括空格。
  • 变量名不能以数字开头。

5.2 变量声明

变量使用前必须先声明,我们可以使用 var 来声明变量,如下4种声明方式:

1、声明变量的类型及初始值

// var [变量名] : [类型] = 值;
var uname:string = "Runoob";

2、声明变量的类型,但没有初始值,变量值会设置为 undefined:

// var [变量名] : [类型];
var uname:string;

3、声明变量并初始值,但不设置类型,该变量可以是任意类型:

// var [变量名] = 值;
var uname = "Runoob";

4、声明变量没有设置类型和初始值,类型可以是任意类型,默认初始值为 undefined:

// var [变量名];
var uname;

5.3 变量声明的实例

var uname:string = "Runoob"; 
var score1:number = 50;
var score2:number = 42.50
var sum = score1 + score2 
console.log("名字: "+uname) 
console.log("第一个科目成绩: "+score1) 
console.log("第二个科目成绩: "+score2) 
console.log("总成绩: "+sum)

**注意:**变量不要使用 name 否则会与 DOM 中的全局 window 对象下的 name 属性出现了重名。

使用 tsc 命令编译以上代码,得到如下 JavaScript 代码:

var uname = "Runoob";
var score1 = 50;
var score2 = 42.50;
var sum = score1 + score2;
console.log("名字: " + uname);
console.log("第一个科目成绩: " + score1);
console.log("第二个科目成绩: " + score2);
console.log("总成绩: " + sum);

执行该 JavaScript 代码输出结果为:

名字: Runoob
第一个科目成绩: 50
第二个科目成绩: 42.5
总成绩: 92.5

TypeScript 遵循强类型,如果将不同的类型赋值给变量会编译错误,如下实例:

var num:number = "hello"     // 这个代码会编译错误

5.4 类型断言(Type Assertion)

类型断言可以用来手动指定一个值的类型,即允许变量从一种类型更改为另一种类型。

语法格式:

<类型>值

或者

值 as 类型

var str = '1' 
var str2:number = <number> <any> str   //str、str2 是 string 类型
console.log(str2)

编译后

var str = '1';
var str2 = str;  //str、str2 是 string 类型
console.log(str2);

输出:1

5.5 类型推断

当类型没有给出时,TypeScript 编译器利用类型推断来推断类型。

如果由于缺乏声明而不能推断出类型,那么它的类型被视作默认的动态 any 类型。

var num = 2;    // 类型推断为 number
console.log("num 变量的值为 "+num); 
num = "12";    // 编译错误
console.log(num);
  • 第一行代码声明了变量 num 并=设置初始值为 2。 注意变量声明没有指定类型。因此,程序使用类型推断来确定变量的数据类型,第一次赋值为 2,num 设置为 number 类型。

  • 第三行代码,当我们再次为变量设置字符串类型的值时,这时编译会错误。因为变量已经设置为了 number 类型。

error TS2322: Type '"12"' is not assignable to type 'number'.

5.6 变量作用域

变量作用域指定了变量定义的位置。

程序中变量的可用性由变量作用域决定。

TypeScript 有以下几种作用域:

  • 全局作用域 − 全局变量定义在程序结构的外部,它可以在你代码的任何位置使用。
  • 类作用域 − 这个变量也可以称为 字段。类变量声明在一个类里头,但在类的方法外面。 该变量可以通过类的对象来访问。类变量也可以是静态的,静态的变量可以通过类名直接访问。
  • 局部作用域 − 局部变量,局部变量只能在声明它的一个代码块(如:方法)中使用。
var global_num = 12          // 全局变量
class Numbers { 
   num_val = 13;             // 实例变量
   static sval = 10;         // 静态变量
   
   storeNum():void { 
      var local_num = 14;    // 局部变量
   } 
} 
console.log("全局变量为: "+global_num)  
console.log(Numbers.sval)   // 静态变量
var obj = new Numbers(); 
console.log("实例变量: "+obj.num_val)

以上代码使用 tsc 命令编译为 JavaScript 代码为:

var global_num = 12; // 全局变量
var Numbers = /** @class */ (function () {
    function Numbers() {
        this.num_val = 13; // 实例变量
    }
    Numbers.prototype.storeNum = function () {
        var local_num = 14; // 局部变量
    };
    Numbers.sval = 10; // 静态变量
    return Numbers;
}());
console.log("全局变量为: " + global_num);
console.log(Numbers.sval); // 静态变量
var obj = new Numbers();
console.log("实例变量: " + obj.num_val);

执行以上 JavaScript 代码,输出结果为:

全局变量为: 12
10
实例变量: 13

如果我们在方法外部调用局部变量 local_num,会报错:

error TS2322: Could not find symbol 'local_num'.

六、运算符

6.1 引言

TypeScript 主要包含以下几种运算:

  • 算术运算符
  • 逻辑运算符
  • 关系运算符
  • 按位运算符
  • 赋值运算符
  • 三元/条件运算符
  • 字符串运算符
  • 类型运算符

6.2 算术运算符

假定 y=5,下面的表格解释了这些算术运算符的操作:

运算符描述例子x 运算结果y 运算结果
+加法x=y+275
-减法x=y-235
*乘法x=y*2105
/除法x=y/22.55
%取模(余数)x=y%215
++自增x=++y
x=y++
6
5
6
6
--自减x=--y
x=y--
4
5
4
4

6.3关系运算符

关系运算符用于计算结果是否为 true 或者 false。

x=5,下面的表格解释了关系运算符的操作:

运算符描述比较返回值
==等于x = =8
x==5
false
true
!=不等于x!=8true
大于x>8false
<小于x<8true
>=大于或等于x>=8false
<=小于或等于x<=8true

6.4 逻辑运算符

逻辑运算符用于测定变量或值之间的逻辑。

给定 x=6 以及 y=3,下表解释了逻辑运算符:

运算符描述例子
&&and(x < 10 && y > 1) 为 true
||or(x==5 || y==5) 为 false
!not!(x==y) 为 true

注意:短路运算符(&& 与 ||):

  • && 与 || 运算符可用于组合表达式。 && 运算符只有在左右两个表达式都为 true 时才返回 true。
  • || 运算符只要其中一个表达式为 true ,则该组合表达式就会返回 true。

6.5 位运算符

位操作是程序设计中对位模式按位或二进制数的一元和二元操作。

运算符描述例子类似于结果十进制
&AND,按位与处理两个长度相同的二进制数,两个相应的二进位都为 1,该位的结果值才为 1,否则为 0。x = 5 & 10101 & 000100011
|OR,按位或处理两个长度相同的二进制数,两个相应的二进位中只要有一个为 1,该位的结果值为 1。x = 5 | 10101 | 000101015
~取反,取反是一元运算符,对一个二进制数的每一位执行逻辑反操作。使数字 1 成为 0,0 成为 1。x = ~ 5~01011010-6
异或,按位异或运算,对等长二进制模式按位或二进制数的每一位执行逻辑异按位或操作。操作的结果是如果某位不同则该位为 1,否则该位为 0。x = 5 ^ 10101 ^ 000101004
<<左移,把 << 左边的运算数的各二进位全部左移若干位,由 << 右边的数指定移动的位数,高位丢弃,低位补 0。x = 5 << 10101 << 1101010
>>右移,把 >> 左边的运算数的各二进位全部右移若干位,>> 右边的数指定移动的位数。x = 5 >> 10101 >> 100102
>>>无符号右移,与有符号右移位类似,除了左边一律使用0 补位。x = 2 >>> 10010 >>> 100011

注意:按位取反~

var temp = ~5;


计算步骤:

5 二进制 101,补满 32位 00000000000000000000000000000101 按位取反 11111111111111111111111111111010 由于32位开头第一个是1,所以这是一个负数,将二进制转换成负数,需要先反码 00000000000000000000000000000101 之后,再+1 00000000000000000000000000000110 转换成十进制为6,加上符号变成负数 -6

负数十进制转二进制:在十进制绝对值原码的基础上取反加一

例如:

-5 如何用二进制表示

取反加1
5 =0000 0101

取反
1111 1010

加1
1111 1011

所以这个就表示 -5

1111 1011 对应十进制中的多少 【减一取反】= 【取反加一】

首位是 1 ,故是负数

减一      
1111 1010

取反           
0000 0101=5

所以答案是    
-5

6.6 赋值运算符

赋值运算符用于给变量赋值。

给定 x=10y=5,下面的表格解释了赋值运算符:

运算符例子实例x 值
= (赋值)x = yx = yx = 5
+= (先进行加运算后赋值)x += yx = x + yx = 15
-= (先进行减运算后赋值)x -= yx = x - yx = 5
*= (先进行乘运算后赋值)x *= yx = x * yx = 50
/= (先进行除运算后赋值)x /= yx = x / yx = 2

类似的逻辑运算符也可以与赋值运算符联合使用:<<=, >>=, >>=, &=, |= 与 ^=。

6.7 三元运算符 (?)

三元运算有 3 个操作数,并且需要判断布尔表达式的值。该运算符的主要是决定哪个值应该赋值给变量。

Test ? expr1 : expr2

6.8 类型运算符

1、typeof 运算符

typeof 是一元运算符,返回操作数的数据类型。

查看以下实例:

var num = 12 
console.log(typeof num);   //输出结果: number

使用 tsc 命令编译以上代码得到如下 JavaScript 代码:

var num = 12;
console.log(typeof num); //输出结果: number

以上实例输出结果如下:

number

2、instanceof:用于判断对象是否为指定的类型

function Person(){};
var p =new Person();
console.log(p instanceof Person);//true

6.9 其他运算符

1、负号运算符(-)

更改操作数的符号,查看以下实例:

var x:number = 4 
var y = -x; 
console.log("x 值为: ",x);   // 输出结果 4 
console.log("y 值为: ",y);   // 输出结果 -4

使用 tsc 命令编译以上代码得到如下 JavaScript 代码:

var x = 4;
var y = -x;
console.log("x 值为: ", x); // 输出结果 4 
console.log("y 值为: ", y); // 输出结果 -4

以上实例输出结果如下:

x 值为:  4
y 值为:  -4

2、字符串运算符: 连接运算符 (+)

+ 运算符可以拼接两个字符串,查看以下实例:

var msg:string = "RUNOOB"+".COM" 
console.log(msg)

使用 tsc 命令编译以上代码得到如下 JavaScript 代码:

var msg = "RUNOOB" + ".COM";
console.log(msg);

以上实例输出结果如下:

RUNOOB.COM

七、函数

7.1 引言

函数是JavaScript应用程序的基础。 它帮助你实现抽象层,模拟类,信息隐藏和模块。 在TypeScript里,虽然已经支持类,命名空间和模块,但函数仍然是主要的定义 行为的地方。 TypeScript为JavaScript函数添加了额外的功能,让我们可以更容易地使用。

和JavaScript一样,TypeScript函数可以创建有名字的函数和匿名函数。 你可以随意选择适合应用程序的方式,不论是定义一系列API函数还是只使用一次的函数。

简单实例:

// Named function
function add(x, y) {
    return x + y;
}

// Anonymous function
let myAdd = function(x, y) { return x + y; };

TypeScript函数实例:

// 函数定义
function greet():string { // 返回一个字符串
    return "Hello World" 
} 
 
function caller() { 
    var msg = greet() // 调用 greet() 函数 
    console.log(msg) 
} 
 
// 调用函数
caller()
  • 实例中定义函数 greet() ,返回值的类型为 string
  • greet() 返回一个字符串

编译以上代码,得到以下 JavaScript 代码:

// 函数定义
function greet() {
    return "Hello World";
}
function caller() {
    var msg = greet(); // 调用 greet() 函数 
    console.log(msg);
}
// 调用函数
caller();

7.2 可选参数和默认参数

在 TypeScript 函数里,如果我们定义了参数,则我们必须传入这些参数,除非将这些参数设置为可选,可选参数使用问号标识 ?。

function buildName(firstName: string, lastName: string) {
    return firstName + " " + lastName;
}
 
let result1 = buildName("Bob");                  // 错误,缺少参数
let result2 = buildName("Bob", "Adams", "Sr.");  // 错误,参数太多了
let result3 = buildName("Bob", "Adams");         // 正确

可选参数

function buildName(firstName: string, lastName?: string) {
    if (lastName)
        return firstName + " " + lastName;
    else
        return firstName;
}
 
let result1 = buildName("Bob");  // 正确
let result2 = buildName("Bob", "Adams", "Sr.");  // 错误,参数太多了
let result3 = buildName("Bob", "Adams");  // 正确

默认参数

以下实例函数的参数 rate 设置了默认值为 0.50,调用该函数时如果未传入参数则使用该默认值

function calculate_discount(price:number,rate:number = 0.50) { 
    var discount = price * rate; 
    console.log("计算结果: ",discount); 
} 
calculate_discount(1000) 
calculate_discount(1000,0.30)

编译以上代码,得到以下 JavaScript 代码:

function calculate_discount(price, rate) {
    if (rate === void 0) { rate = 0.50; }
    var discount = price * rate;
    console.log("计算结果: ", discount);
}
calculate_discount(1000);// 500
calculate_discount(1000, 0.30);//300

剩余参数

有一种情况,我们不知道要向函数传入多少个参数,这时候我们就可以使用剩余参数来定义。

剩余参数语法允许我们将一个不确定数量的参数作为一个数组传入。

function buildName(firstName: string, ...restOfName: string[]) {
    return firstName + " " + restOfName.join(" ");
}
  
let employeeName = buildName("Joseph", "Samuel", "Lucas", "MacKinzie");

函数的最后一个命名参数 restOfName 以 ... 为前缀,它将成为一个由剩余参数组成的数组,索引值从0(包括)到 restOfName.length(不包括)。

7.3 匿名函数

匿名函数是一个没有函数名的函数。

匿名函数在程序运行时动态声明,除了没有函数名外,其他的与标准函数一样。

我们可以将匿名函数赋值给一个变量,这种表达式就成为函数表达式。

var res = function(a:number,b:number) { 
    return a*b;  
}; 
console.log(res(12,2))

编译以上代码,得到以下 JavaScript 代码:

var res = function (a, b) {
    return a * b;
};
console.log(res(12, 2)); // 24

匿名函数自调用

(function () { 
    var x = "Hello!!";   
    console.log(x)     
})()

7.4 构造函数

TypeScript 也支持使用 JavaScript 内置的构造函数 Function() 来定义函数

var myFunction = new Function("a", "b", "return a * b"); 
var x = myFunction(4, 3); 
console.log(x);

编译以上代码,得到以下 JavaScript 代码:

var myFunction = new Function("a", "b", "return a * b"); 
var x = myFunction(4, 3); 
console.log(x);//12

7.5 递归函数

递归函数即在函数内调用函数本身。

举个例子: 从前有座山,山里有座庙,庙里有个老和尚,正在给小和尚讲故事呢!故事是什么呢?"从前有座山,山里有座庙,庙里有个老和尚,正在给小和尚讲故事呢!故事是什么呢?'从前有座山,山里有座庙,庙里有个老和尚,正在给小和尚讲故事呢!故事是什么呢?……'"

function factorial(number) {
    if (number <= 0) {         // 停止执行
        return 1; 
    } else {     
        return (number * factorial(number - 1));     // 调用自身
    } 
}; 
console.log(factorial(6));      // 输出 720

编译以上代码,得到以下 JavaScript 代码:

function factorial(number) {
    if (number <= 0) { // 停止执行
        return 1;
    }
    else {
        return (number * factorial(number - 1)); // 调用自身
    }
}
;
console.log(factorial(6)); // 输出 720

7.6 Lambda函数

Lambda 函数也称之为箭头函数。

箭头函数表达式的语法比函数表达式更短。

var foo = (x:number)=>10 + x 
console.log(foo(100))      //输出结果为 110

编译以上代码,得到以下 JavaScript 代码:

var foo = function (x) { return 10 + x; };
console.log(foo(100)); //输出结果为 110

单个参数x可以这样定义

var display = x => { 
    console.log("输出为 "+x) 
} 
display(12)

无参数时可以设置空括号:

var disp =()=> { 
    console.log("Function invoked"); 
} 
disp();

7.7 函数重载

重载是方法名字相同,而参数不同,返回类型可以相同也可以不同。

每个重载的方法(或者构造函数)都必须有一个独一无二的参数类型列表。

以下实例定义了参数类型与参数数量不同

function disp(s1:string):void; 
function disp(n1:number,s1:string):void; 
 
function disp(x:any,y?:any):void { 
    console.log(x); 
    console.log(y); 
} 
disp("abc") 
disp(1,"xyz");

注意:

  • 如果参数类型不同,则参数类型应设置为 any

  • 参数数量不同你可以将不同的参数设置为可选

八、重点类型

8.1 Array数组

数组声明:

var array_name[:datatype];        //声明 
array_name = [val1,val2,valn..]   //初始化

示例

var sites:string[]; 
sites = ["Google","Runoob","Taobao"]

8.2 Map对象

Map 对象保存键值对,并且能够记住键的原始插入顺序。

任何值(对象或者原始值) 都可以作为一个键或一个值。

Map 是 ES6 中引入的一种新的数据结构,可以参考 ES6 Map 与 Set

创建Map

let myMap = new Map();

初始化 Map,可以以数组的格式来传入键值对:

let myMap = new Map([
        ["key1", "value1"],
        ["key2", "value2"]
    ]); 

Map 相关的函数与属性:

  • map.clear() – 移除 Map 对象的所有键/值对 。
  • map.set() – 设置键值对,返回该 Map 对象。
  • map.get() – 返回键对应的值,如果不存在,则返回 undefined。
  • map.has() – 返回一个布尔值,用于判断 Map 中是否包含键对应的值。
  • map.delete() – 删除 Map 中的元素,删除成功返回 true,失败返回 false。
  • map.size – 返回 Map 对象键/值对的数量。
  • map.keys() - 返回一个 Iterator 对象, 包含了 Map 对象中每个元素的键 。
  • map.values() – 返回一个新的Iterator对象,包含了Map对象中每个元素的值 。

遍历Map

let nameSiteMapping = new Map();
 
nameSiteMapping.set("Google", 1);
nameSiteMapping.set("Runoob", 2);
nameSiteMapping.set("Taobao", 3);
 
// 迭代 Map 中的 key
for (let key of nameSiteMapping.keys()) {
    console.log(key);                  
}
 
// 迭代 Map 中的 value
for (let value of nameSiteMapping.values()) {
    console.log(value);                 
}
 
// 迭代 Map 中的 key => value
for (let entry of nameSiteMapping.entries()) {
    console.log(entry[0], entry[1]);   
}
 
// 使用对象解析
for (let [key, value] of nameSiteMapping) {
    console.log(key, value);            
}

使用 es6 编译:

tsc --target es6 test.ts

编译以上代码得到如下 JavaScript 代码:

实例

let nameSiteMapping = new Map();
nameSiteMapping.set("Google", 1);
nameSiteMapping.set("Runoob", 2);
nameSiteMapping.set("Taobao", 3);
// 迭代 Map 中的 key
for (let key of nameSiteMapping.keys()) {
    console.log(key);
}
// 迭代 Map 中的 value
for (let value of nameSiteMapping.values()) {
    console.log(value);
}
// 迭代 Map 中的 key => value
for (let entry of nameSiteMapping.entries()) {
    console.log(entry[0], entry[1]);
}
// 使用对象解析
for (let [key, value] of nameSiteMapping) {
    console.log(key, value);
}

执行以上 JavaScript 代码,输出结果为:

Google
Runoob
Taobao
1
2
3
Google 1
Runoob 2
Taobao 3
Google 1
Runoob 2

8.3 联合类型

联合类型(Union Types)可以通过管道(|)将变量设置多种类型,赋值时可以根据设置的类型来赋值。而且只能指定的类型,如果赋值其它类型就会报错。

var val:string|number 
val = 12 
console.log("数字为 "+ val) 
val = "Runoob" 
console.log("字符串为 " + val)

tsc编译后得到js代码:

var val;
val = 12;
console.log("数字为 " + val);//数字为 12
val = "Runoob";
console.log("字符串为 " + val);//字符串为 Runoob

联合类型数组

var arr:number[]|string[]; 
var i:number; 
arr = [1,2,4] 
console.log("**数字数组**")  
 
for(i = 0;i<arr.length;i++) { 
   console.log(arr[i]) 
}  
 
arr = ["Runoob","Google","Taobao"] 
console.log("**字符串数组**")  
 
for(i = 0;i<arr.length;i++) { 
   console.log(arr[i]) 
}

tsc编译后得到的js代码:

var arr;
var i;
arr = [1, 2, 4];
console.log("**数字数组**");
for (i = 0; i < arr.length; i++) {
    console.log(arr[i]);
}
arr = ["Runoob", "Google", "Taobao"];
console.log("**字符串数组**");
for (i = 0; i < arr.length; i++) {
    console.log(arr[i]);
}

执行结果:

**数字数组**
1
2
4
**字符串数组**
Runoob
Google
Taobao

九、接口

9.1 引言

接口是一系列抽象方法的声明,是一些方法特征的集合,这些方法都应该是抽象的,需要由具体的类去实现。TypeScript接口定义如下:

interface interface_name { 
}

9.2 接口实例

以下实例中,我们定义了一个接口 IPerson,接着定义了一个变量 customer,它的类型是 IPerson。

customer 实现了接口 IPerson 的属性和方法。

interface IPerson { 
    firstName:string, 
    lastName:string, 
    sayHi: ()=>string 
} 
 
var customer:IPerson = { 
    firstName:"Tom",
    lastName:"Hanks", 
    sayHi: ():string =>{return "Hi there"} 
} 
 
console.log("Customer 对象 ") 
console.log(customer.firstName) 
console.log(customer.lastName) 
console.log(customer.sayHi())  
 
var employee:IPerson = { 
    firstName:"Jim",
    lastName:"Blakes", 
    sayHi: ():string =>{return "Hello!!!"} 
} 
 
console.log("Employee  对象 ") 
console.log(employee.firstName) 
console.log(employee.lastName)

需要注意接口不能转换为 JavaScript。 它只是 TypeScript 的一部分。

编译以上代码,得到以下 JavaScript 代码:

var customer = {
    firstName: "Tom",
    lastName: "Hanks",
    sayHi: function () { return "Hi there"; }
};
console.log("Customer 对象 ");
console.log(customer.firstName);
console.log(customer.lastName);
console.log(customer.sayHi());
var employee = {
    firstName: "Jim",
    lastName: "Blakes",
    sayHi: function () { return "Hello!!!"; }
};
console.log("Employee  对象 ");
console.log(employee.firstName);
console.log(employee.lastName);

输出结果

Customer 对象
Tom
Hanks
Hi there
Employee  对象
Jim
Blakes

9.3 联合类型和接口

interface RunOptions { 
    program:string; 
    commandline:string[]|string|(()=>string); 
} 
 
// commandline 是字符串
var options:RunOptions = {program:"test1",commandline:"Hello"}; 
console.log(options.commandline)  
 
// commandline 是字符串数组
options = {program:"test1",commandline:["Hello","World"]}; 
console.log(options.commandline[0]); 
console.log(options.commandline[1]);  
 
// commandline 是一个函数表达式
options = {program:"test1",commandline:()=>{return "**Hello World**";}}; 
 
var fn:any = options.commandline; 
console.log(fn());

编译以上代码,得到以下 JavaScript 代码:

// commandline 是字符串
var options = { program: "test1", commandline: "Hello" };
console.log(options.commandline);
// commandline 是字符串数组
options = { program: "test1", commandline: ["Hello", "World"] };
console.log(options.commandline[0]);
console.log(options.commandline[1]);
// commandline 是一个函数表达式
options = { program: "test1", commandline: function () { return "**Hello World**"; } };
var fn = options.commandline;
console.log(fn());

输出结果为:

Hello
Hello
World
**Hello World**

9.4 接口和数组

接口中我们可以将数组的索引值和元素设置为不同类型,索引值可以是数字或字符串。

interface namelist { 
   [index:number]:string 
} 
 
var list2:namelist = ["John",1,"Bran"] // 错误元素 1 不是 string 类型
interface ages { 
   [index:string]:number 
} 
 
var agelist:ages; 
agelist["John"] = 15   // 正确 
agelist[2] = "nine"   // 错误

9.5 接口继承

接口继承:接口可以通过其他接口扩展自己

  • TypeScript接口可以多继承
  • 继承使用的关键字 extends

单继承语法格式

Child_interface_name extends super_interface_name

多继承语法格式

Child_interface_name extends super_interface1_name, super_interface2_name,…,super_interfaceN_name

单继承实例

interface Person { 
   age:number 
} 
 
interface Musician extends Person { 
   instrument:string 
} 
 
var drummer = <Musician>{}; 
drummer.age = 27 
drummer.instrument = "Drums" 
console.log("年龄:  "+drummer.age)
console.log("喜欢的乐器:  "+drummer.instrument)

编译以上代码,得到以下 JavaScript 代码:

var drummer = {};
drummer.age = 27;
drummer.instrument = "Drums";
console.log("年龄:  " + drummer.age);
console.log("喜欢的乐器:  " + drummer.instrument);

输出结果:

年龄:  27
喜欢的乐器:  Drums

多继承实例

interface IParent1 { 
    v1:number 
} 
 
interface IParent2 { 
    v2:number 
} 
 
interface Child extends IParent1, IParent2 { } 
var Iobj:Child = { v1:12, v2:23} 
console.log("value 1: "+Iobj.v1+" value 2: "+Iobj.v2)

编译以上代码,得到以下 JavaScript 代码:

var Iobj = { v1: 12, v2: 23 };
console.log("value 1: " + Iobj.v1 + " value 2: " + Iobj.v2);

输出结果为:

value 1: 12 value 2: 23

十、类

10.1 引言

传统的JavaScript程序使用函数和基于原型的继承来创建可重用的组件,但对于熟悉使用面向对象方式的程序员来讲就有些棘手,因为他们用的是基于类的继承并且对象是由类构建出来的。 从ECMAScript 2015,也就是ECMAScript 6开始,JavaScript程序员将能够使用基于类的面向对象的方式。 使用TypeScript,我们允许开发者现在就使用这些特性,并且编译后的JavaScript可以在所有主流浏览器和平台上运行,而不需要等到下个JavaScript版本。

class Greeter {
    greeting: string;
    constructor(message: string) {
        this.greeting = message;
    }
    greet() {
        return "Hello, " + this.greeting;
    }
}

let greeter = new Greeter("world");

10.2 继承

在TypeScript里,我们可以使用常用的面向对象模式。 基于类的程序设计中一种最基本的模式是允许使用继承来扩展现有的类。

class Animal {
    move(distanceInMeters: number = 0) {
        console.log(`Animal moved ${distanceInMeters}m.`);
    }
}

class Dog extends Animal {
    bark() {
        console.log('Woof! Woof!');
    }
}

const dog = new Dog();
dog.bark();
dog.move(10);
dog.bark();

这个例子展示了最基本的继承:类从基类中继承了属性和方法。 这里, Dog是一个 派生类,它派生自 Animal 基类,通过 extends关键字。 派生类通常被称作 子类,基类通常被称作 超类

因为 Dog继承了 Animal的功能,因此我们可以创建一个 Dog的实例,它能够 bark()move()


class Animal {
    name: string;
    constructor(theName: string) { this.name = theName; }
    move(distanceInMeters: number = 0) {
        console.log(`${this.name} moved ${distanceInMeters}m.`);
    }
}

class Snake extends Animal {
    constructor(name: string) { super(name); }
    move(distanceInMeters = 5) {
        console.log("Slithering...");
        super.move(distanceInMeters);
    }
}

class Horse extends Animal {
    constructor(name: string) { super(name); }
    move(distanceInMeters = 45) {
        console.log("Galloping...");
        super.move(distanceInMeters);
    }
}

let sam = new Snake("Sammy the Python");
let tom: Animal = new Horse("Tommy the Palomino");

sam.move();
tom.move(34);

这个例子展示了一些上面没有提到的特性。 这一次,我们使用 extends关键字创建了 Animal的两个子类:HorseSnake

与前一个例子的不同点是,派生类包含了一个构造函数,它 必须调用 super(),它会执行基类的构造函数。 而且,在构造函数里访问 this的属性之前,我们 一定要调用 super()。 这个是TypeScript强制执行的一条重要规则。

十一、对象

11.1 引言

对象是包含一组键值对的实例。 值可以是标量、函数、数组、对象等,如下实例

// 以下对象包含了标量,函数,集合(数组或元组)。
var object_name = { 
    key1: "value1", // 标量
    key2: "value",  
    key3: function() {
        // 函数
    }, 
    key4:["content1", "content2"] //集合
}

11.2 类型模板

假如我们在 JavaScript 定义了一个对象:

var sites = { 
   site1:"Runoob", 
   site2:"Google" 
};

这时如果我们想在对象中添加方法,可以做以下修改:

sites.sayHello = function(){ return "hello";}

如果在 TypeScript 中使用以上方式则会出现编译错误,因为Typescript 中的对象必须是特定类型的实例。


var sites = {
    site1: "Runoob",
    site2: "Google",
    sayHello: function () { } // 类型模板
};
sites.sayHello = function () {
    console.log("hello " + sites.site1);
};
sites.sayHello();

此外对象也可以作为一个参数传递给函数,如下实例:

var sites = { 
    site1:"Runoob", 
    site2:"Google",
}; 
var invokesites = function(obj: { site1:string, site2 :string }) { 
    console.log("site1 :"+obj.site1) 
    console.log("site2 :"+obj.site2) 
} 
invokesites(sites)

编译以上代码,得到以下 JavaScript 代码:

var sites = {
    site1: "Runoob",
    site2: "Google"
};
var invokesites = function (obj) {
    console.log("site1 :" + obj.site1);
    console.log("site2 :" + obj.site2);
};
invokesites(sites);

输出结果:

site1 :Runoob
site2 :Google

十二、命名空间

这篇文章描述了如何在TypeScript里使用命名空间(之前叫做“内部模块”)来组织你的代码。

12.1 引言

命名空间一个最明确的目的就是解决重名问题。

假设这样一种情况,当一个班上有两个名叫小明的学生时,为了明确区分它们,我们在使用名字之外,不得不使用一些额外的信息,比如他们的姓(王小明,李小明),或者他们父母的名字等等。

命名空间定义了标识符的可见范围,一个标识符可在多个名字空间中定义,它在不同名字空间中的含义是互不相干的。这样,在一个新的名字空间中可定义任何标识符,它们不会与任何已有的标识符发生冲突,因为已有的定义都处于其他名字空间中。

TypeScript 中命名空间使用 namespace 来定义,语法格式如下:

namespace SomeNameSpaceName { 
   export interface ISomeInterfaceName {      }  
   export class SomeClassName {      }  
}

以上定义了一个命名空间 SomeNameSpaceName,如果我们需要在外部可以调用 SomeNameSpaceName 中的类和接口,则需要在类和接口添加 export 关键字。

要在另外一个命名空间调用语法格式为:

SomeNameSpaceName.SomeClassName;

如果一个命名空间在一个单独的 TypeScript 文件中,则应使用三斜杠 /// 引用它,语法格式如下:

/// <reference path = "SomeFileName.ts" />

以下实例演示了命名空间的使用,定义在不同文件中:

// IShape.ts 文件代码:
namespace Drawing { 
    export interface IShape { 
        draw(); 
    }
}
// Circle.ts 文件代码:
/// <reference path = "IShape.ts" /> 
namespace Drawing { 
    export class Circle implements IShape { 
        public draw() { 
            console.log("Circle is drawn"); 
        }  
    }
}
// Triangle.ts 文件代码:
/// <reference path = "IShape.ts" /> 
namespace Drawing { 
    export class Triangle implements IShape { 
        public draw() { 
            console.log("Triangle is drawn"); 
        } 
    } 
}
// TestShape.ts 文件代码:
/// <reference path = "IShape.ts" />   
/// <reference path = "Circle.ts" /> 
/// <reference path = "Triangle.ts" />  
function drawAllShapes(shape:Drawing.IShape) { 
    shape.draw(); 
} 
drawAllShapes(new Drawing.Circle());
drawAllShapes(new Drawing.Triangle());

使用 tsc 命令编译以上代码:

tsc --out app.js TestShape.ts  

得到以下 JavaScript 代码:

/// <reference path = "IShape.ts" /> 
var Drawing;
(function (Drawing) {
    var Circle = /** @class */ (function () {
        function Circle() {
        }
        Circle.prototype.draw = function () {
            console.log("Circle is drawn");
        };
        return Circle;
    }());
    Drawing.Circle = Circle;
})(Drawing || (Drawing = {}));
/// <reference path = "IShape.ts" /> 
var Drawing;
(function (Drawing) {
    var Triangle = /** @class */ (function () {
        function Triangle() {
        }
        Triangle.prototype.draw = function () {
            console.log("Triangle is drawn");
        };
        return Triangle;
    }());
    Drawing.Triangle = Triangle;
})(Drawing || (Drawing = {}));
/// <reference path = "IShape.ts" />   
/// <reference path = "Circle.ts" /> 
/// <reference path = "Triangle.ts" />  
function drawAllShapes(shape) {
    shape.draw();
}
drawAllShapes(new Drawing.Circle());
drawAllShapes(new Drawing.Triangle());

使用 node 命令查看输出结果为:

$ node app.js
Circle is drawn
Triangle is drawn

12.2 嵌套命名空间

命名空间支持嵌套,即你可以将命名空间定义在另外一个命名空间里头。

// Invoice.ts 文件代码:
namespace Runoob { 
   export namespace invoiceApp { 
      export class Invoice { 
         public calculateDiscount(price: number) { 
            return price * .40; 
         } 
      } 
   } 
}
// InvoiceTest.ts 文件代码:
/// <reference path = "Invoice.ts" />
var invoice = new Runoob.invoiceApp.Invoice(); 
console.log(invoice.calculateDiscount(500));

使用 tsc 命令编译以上代码:

tsc --out app.js InvoiceTest.ts

得到以下 JavaScript 代码:

var Runoob;
(function (Runoob) {
    var invoiceApp;
    (function (invoiceApp) {
        var Invoice = /** @class */ (function () {
            function Invoice() {
            }
            Invoice.prototype.calculateDiscount = function (price) {
                return price * .40;
            };
            return Invoice;
        }());
        invoiceApp.Invoice = Invoice;
    })(invoiceApp = Runoob.invoiceApp || (Runoob.invoiceApp = {}));
})(Runoob || (Runoob = {}));
/// <reference path = "Invoice.ts" />
var invoice = new Runoob.invoiceApp.Invoice();
console.log(invoice.calculateDiscount(500));

使用 node 命令查看输出结果为:

$ node app.js
200

12.3 别名

一种简化命名空间操作的方法是使用import q = x.y.z给常用的对象起一个短的名字。 不要与用来加载模块的import x = require('name')语法弄混了,这里的语法是为指定的符号创建一个别名。 你可以用这种方法为任意标识符创建别名,也包括导入的模块中的对象。

namespace Shapes {
    export namespace Polygons {
        export class Triangle { }
        export class Square { }
    }
}

import polygons = Shapes.Polygons;
let sq = new polygons.Square(); // Same as "new Shapes.Polygons.Square()"

注意,我们并没有使用require关键字,而是直接使用导入符号的限定名赋值。 这与使用 var相似,但它还适用于类型和导入的具有命名空间含义的符号。 重要的是,对于值来讲, import会生成与原始符号不同的引用,所以改变别名的var值并不会影响原始变量的值。

12.4 外部命名空间

流行的程序库D3在全局对象d3里定义它的功能。 因为这个库通过一个 <script>标签加载(不是通过模块加载器),它的声明文件使用内部模块来定义它的类型。 为了让TypeScript编译器识别它的类型,我们使用外部命名空间声明。 比如,我们可以像下面这样写:

declare namespace D3 {
    export interface Selectors {
        select: {
            (selector: string): Selection;
            (element: EventTarget): Selection;
        };
    }

    export interface Event {
        x: number;
        y: number;
    }

    export interface Base extends Selectors {
        event: Event;
    }
}

declare var d3: D3.Base;

十三、模块

TypeScript 1.5里术语名已经发生了变化。 “内部模块”现在称做“命名空间”。 “外部模块”现在则简称为“模块”,这是为了与 ECMAScript 2015里的术语保持一致,(也就是说 module X { 相当于现在推荐的写法 namespace X {)。

13.1 引言

从ECMAScript 2015开始,JavaScript引入了模块的概念。TypeScript也沿用这个概念。

  • 模块在其自身的作用域里执行,而不是在全局作用域里
  • 使用export形式导出内部函数、变量、类等,使他们外部可见,可使用。
  • 其他模块使用 import 形式导入模块,使用其中的函数、变量、类等。

模块导出使用关键字 export 关键字,语法格式如下:

// 文件名 : SomeInterface.ts 
export interface SomeInterface { 
   // 代码部分
}

要在另外一个文件使用该模块就需要使用 import 关键字来导入:

import someInterfaceRef = require("./SomeInterface");

13.2 导出export

重命名:在导出时我们可能需要部分重命名,可以如下写:

// Validation.ts
export interface StringValidator {
    isAcceptable(s: string): boolean;
}
// ZipCodeValidator.ts
class ZipCodeValidator implements StringValidator {
    isAcceptable(s: string) {
        return s.length === 5 && numberRegexp.test(s);
    }
}
export { ZipCodeValidator };
export { ZipCodeValidator as mainValidator };

重新导出:我们经常会去扩展其它模块,并且只导出那个模块的部分内容。 重新导出功能并不会在当前模块导入那个模块或定义一个新的局部变量。

// ParseIntBasedZipCodeValidator.ts
export class ParseIntBasedZipCodeValidator {
    isAcceptable(s: string) {
        return s.length === 5 && parseInt(s).toString() === s;
    }
}

// 导出原先的验证器但做了重命名
export {ZipCodeValidator as RegExpBasedZipCodeValidator} from "./ZipCodeValidator";

或者一个模块可以包裹多个模块,并把他们导出的内容联合在一起通过语法:export * from "module"

// AllValidators.ts
export * from "./StringValidator"; // exports interface StringValidator
export * from "./LettersOnlyValidator"; // exports class LettersOnlyValidator
export * from "./ZipCodeValidator";  // exports class ZipCodeValidator

13.3 导入import

模块的导入操作与导出一样简单。 可以使用以下 import形式之一来导入其它模块中的导出内容。

导入一个模块中的某个导出内容

import { ZipCodeValidator } from "./ZipCodeValidator";

let myValidator = new ZipCodeValidator();

可以对导入内容重命名

import { ZipCodeValidator as ZCV } from "./ZipCodeValidator";
let myValidator = new ZCV();

将整个模块导入到一个变量,并通过它来访问模块的导出部分

import * as validator from "./ZipCodeValidator";
let myValidator = new validator.ZipCodeValidator();

具有副作用的导入模块

尽管不推荐这么做,一些模块会设置一些全局状态供其它模块使用。 这些模块可能没有任何的导出或用户根本就不关注它的导出。 使用下面的方法来导入这类模块:

import "./my-module.js";

13.4 默认导出

每个模块都可以有一个default导出。 默认导出使用 default关键字标记;并且一个模块只能够有一个default导出。 需要使用一种特殊的导入形式来导入 default导出。

default导出十分便利。 比如,像JQuery这样的类库可能有一个默认导出 jQuery$,并且我们基本上也会使用同样的名字jQuery$导出JQuery。

// JQuery.d.ts
declare let $: JQuery;
export default $;
// App.ts
import $ from "JQuery";

$("button.continue").html( "Next Step..." );

类和函数声明可以直接被标记为默认导出。 标记为默认导出的类和函数的名字是可以省略的。例如:

// ZipCodeValidator.ts
export default class ZipCodeValidator {
    static numberRegexp = /^[0-9]+$/;
    isAcceptable(s: string) {
        return s.length === 5 && ZipCodeValidator.numberRegexp.test(s);
    }
}
// Test.ts
import validator from "./ZipCodeValidator";

let myValidator = new validator();

另一个实例:

// StaticZipCodeValidator.ts
const numberRegexp = /^[0-9]+$/;

export default function (s: string) {
    return s.length === 5 && numberRegexp.test(s);
}
// Test.ts
import validate from "./StaticZipCodeValidator";

let strings = ["Hello", "98052", "101"];

// Use function validate
strings.forEach(s => {
  console.log(`"${s}" ${validate(s) ? " matches" : " does not match"}`);
});

default导出也可以是一个值

// OneTwoThree.ts
export default "123";
// Log.ts
import num from "./OneTwoThree";

console.log(num); // "123"

13.5 export =和import = require()

CommonJS和AMD的环境里都有一个exports变量,这个变量包含了一个模块的所有导出内容。

CommonJS和AMD的exports都可以被赋值为一个对象, 这种情况下其作用就类似于 es6 语法里的默认导出,即 export default语法了。虽然作用相似,但是 export default 语法并不能兼容CommonJS和AMD的exports

为了支持CommonJS和AMD的exports, TypeScript提供了export =语法。

export =语法定义一个模块的导出对象。 这里的对象一词指的是类,接口,命名空间,函数或枚举。

若使用export =导出一个模块,则必须使用TypeScript的特定语法import module = require("module")来导入此模块。

// ZipCodeValidator.ts
let numberRegexp = /^[0-9]+$/;
class ZipCodeValidator {
    isAcceptable(s: string) {
        return s.length === 5 && numberRegexp.test(s);
    }
}
export = ZipCodeValidator;
// Test.ts
import zip = require("./ZipCodeValidator");

// Some samples to try
let strings = ["Hello", "98052", "101"];

// Validators to use
let validator = new zip();

// Show whether each string passed each validator
strings.forEach(s => {
  console.log(`"${ s }" - ${ validator.isAcceptable(s) ? "matches" : "does not match" }`);
});

有时候,你只想在某种条件下才加载某个模块。编译器会检测是否每个模块都会在生成的JavaScript中用到。 如果一个模块标识符只在类型注解部分使用,并且完全没有在表达式中使用时,就不会生成 require这个模块的代码。 省略掉没有用到的引用对性能提升是很有益的,并同时提供了选择性加载模块的能力。

理解:TypeScript在编译时,检测到import xxx require(xxxx) 没有用到(或者在注解中用到),编译后这句话为空(不进行编译)

13.6 使用其它的JavaScript库

要想描述非TypeScript编写的类库的类型,我们需要声明类库所暴露出的API。

我们叫它声明因为它不是“外部程序”的具体实现。 它们通常是在 .d.ts文件里定义的。 如果你熟悉C/C++,你可以把它们当做 .h文件。 让我们看一些例子。

外部模块

在Node.js里大部分工作是通过加载一个或多个模块实现的。 我们可以使用顶级的 export声明来为每个模块都定义一个.d.ts文件,但最好还是写在一个大的.d.ts文件里。 我们使用与构造一个外部命名空间相似的方法,但是这里使用 module关键字并且把名字用引号括起来,方便之后import。 例如:

// node.d.ts (simplified excerpt)
declare module "url" {
    export interface Url {
        protocol?: string;
        hostname?: string;
        pathname?: string;
    }

    export function parse(urlStr: string, parseQueryString?, slashesDenoteHost?): Url;
}

declare module "path" {
    export function normalize(p: string): string;
    export function join(...paths: any[]): string;
    export let sep: string;
}

现在我们可以/// <reference> node.d.ts并且使用import url = require("url");import * as URL from "url"加载模块。

/// <reference path="node.d.ts"/>
import * as URL from "url";
let myUrl = URL.parse("http://www.typescriptlang.org");

外部模块简写

假如你不想在使用一个新模块之前花时间去编写声明,你可以采用声明的简写形式以便能够快速使用它

// declarations.d.ts
declare module "hot-new-module";

简写模块里所有导出的类型将是any

import x, {y} from "hot-new-module";
x(y);

13.7 模块里不要使用命名空间

  • 模块具有其自己的作用域,并且只有导出的声明才会在模块外部可见。 命名空间在使用模块时几乎没什么价值。
  • 模块本身存在于文件系统之中,我们必须通过路径和文件名找到它们。我们可以创建 /collections/generic/文件夹,把相应模块放在这里面。
  • 命名空间是解决全局作用域里命名冲突,My.Application.Customer.AddFormMy.Application.Order.AddForm -- 两个类型的名字相同,但命名空间不同。
  • 在一个模块里,没有理由两个对象拥有同一个名字。 从模块的使用角度来说,使用者会挑出他们用来引用模块的名字,所以也没有理由发生重名的情况。

13.8 填坑问题

以下均为模块结构上的危险信号。重新检查以确保你没有在对模块使用命名空间:

  • 文件的顶层声明是export namespace Foo { ... } (删除Foo并把所有内容向上层移动一层)
  • 文件只有一个export classexport function (考虑使用export default
  • 多个文件的顶层具有同样的export namespace Foo { (不要以为这些会合并到一个Foo中!)

十四、声明文件

14.1 引言

TypeScript 作为 JavaScript 的超集,在开发过程中不可避免要引用其他第三方的 JavaScript 的库。虽然通过直接引用可以调用库的类和方法,但是却无法使用TypeScript 诸如类型检查等特性功能。为了解决这个问题,需要将这些库里的函数和方法体去掉后只保留导出类型声明,而产生了一个描述 JavaScript 库和模块信息的声明文件。通过引用这个声明文件,就可以借用 TypeScript 的各种特性来使用库文件了。

假如我们想使用第三方库,比如 jQuery,我们通常这样获取一个 id 是 foo 的元素:

$('#foo');
// 或
jQuery('#foo');// 但是在 TypeScript 中,我们并不知道 $ 或 jQuery 是什么东西:

这时,我们需要使用 declare 关键字来定义它的类型,帮助 TypeScript 判断我们传入的参数类型对不对:

declare var jQuery: (selector: string) => any;

jQuery('#foo');

declare 定义的类型只会用于编译时的检查,编译结果中会被删除。

上例的编译结果是:

jQuery('#foo');

14.2 声明文件

声明文件以 .d.ts 为后缀,例如:

runoob.d.ts

声明文件或模块的语法格式如下:

declare module Module_Name {
}

TypeScript 引入声明文件语法格式:

/// <reference path = " runoob.d.ts" />

当然,很多流行的第三方库的声明文件不需要我们定义了,比如 jQuery 已经有人帮我们定义好了:jQuery in DefinitelyTyped

14.3 实例

以下定义一个第三方库来演示:

// CalcThirdPartyJsLib.js 文件代码:
var Runoob;  
(function(Runoob) {
    var Calc = (function () { 
        function Calc() { 
        } 
    })
    Calc.prototype.doSum = function (limit) {
        var sum = 0; 
 
        for (var i = 0; i <= limit; i++) { 
            sum = sum + i; 
        }
        return sum; 
    }
    Runoob.Calc = Calc; 
    return Calc; 
})(Runoob || (Runoob = {})); 
var test = new Runoob.Calc();

如果我们想在 TypeScript 中引用上面的代码,则需要设置声明文件 Calc.d.ts,代码如下:

// Calc.d.ts 文件代码:
declare module Runoob { 
   export class Calc { 
      doSum(limit:number) : number; 
   }
}

声明文件不包含实现,它只是类型声明,把声明文件加入到 TypeScript 中:

// CalcTest.ts 文件代码:
/// <reference path = "Calc.d.ts" /> 
var obj = new Runoob.Calc(); 
// obj.doSum("Hello"); // 编译错误,因为我们需要传入数字参数
console.log(obj.doSum(10));

使用 tsc 命令来编译以上代码文件:

tsc CalcTest.ts

生成的 JavaScript 代码如下:

// CalcTest.js 文件代码:
/// <reference path = "Calc.d.ts" /> 
var obj = new Runoob.Calc();
//obj.doSum("Hello"); // 编译错误,因为我们需要传入数字参数
console.log(obj.doSum(10));

最后我们编写一个 runoob.html 文件,引入 CalcTest.js 文件及第三方库 CalcThirdPartyJsLib.js:

<!DOCTYPE html>
<html>
<head>
<meta charset="utf-8">
<title>菜鸟教程(runoob.com)</title>
<script src = "CalcThirdPartyJsLib.js"></script> 
<script src = "CalcTest.js"></script> 
</head>
<body>
    <h1>声明文件测试</h1>
    <p>菜鸟测试一下。</p>
</body>
</html>

浏览器结果:

image-20210309175149948

十五、泛型

15.1 引言

软件工程中,我们不仅要创建一致的定义良好的API,同时也要考虑可重用性。 组件不仅能够支持当前的数据类型,同时也能支持未来的数据类型,这在创建大型系统时为你提供了十分灵活的功能。

在像C#和Java这样的语言中,可以使用泛型来创建可重用的组件,一个组件可以支持多种类型的数据。 这样用户就可以以自己的数据类型来使用组件。

15.2 泛型之Hello World

下面来创建第一个使用泛型的例子:identity函数。 这个函数会返回任何传入它的值。 你可以把这个函数当成是echo命令。

不用泛型的话,这个函数可能是下面这样:

function identity(arg: number): number {
    return arg;
}

或者,我们使用any类型来定义函数:

function identity(arg: any): any {
    return arg;
}

使用any类型会导致这个函数可以接收任何类型的arg参数,这样就丢失了一些信息:传入的类型与返回的类型应该是相同的。如果我们传入一个数字,我们只知道任何类型的值都有可能被返回。

因此,我们需要一种方法使返回值的类型与传入参数的类型是相同的。 这里,我们使用了 类型变量,它是一种特殊的变量,只用于表示类型而不是值。

function identity<T>(arg: T): T {
    return arg;
}

我们给identity添加了类型变量TT帮助我们捕获用户传入的类型(比如:number),之后我们就可以使用这个类型。 之后我们再次使用了 T当做返回值类型。现在我们可以知道参数类型与返回值类型是相同的了。 这允许我们跟踪函数里使用的类型的信息。

我们把这个版本的identity函数叫做泛型,因为它可以适用于多个类型。 不同于使用 any,它不会丢失信息,像第一个例子那像保持准确性,传入数值类型并返回数值类型。

我们定义了泛型函数后,可以用两种方法使用。 第一种是,传入所有的参数,包含类型参数:

let output = identity<string>("myString");  // type of output will be 'string'

这里我们明确的指定了Tstring类型,并做为一个参数传给函数,使用了<>括起来而不是()

第二种方法更普遍。利用了类型推论 -- 即编译器会根据传入的参数自动地帮助我们确定T的类型:

let output = identity("myString");  // type of output will be 'string'

注意我们没必要使用尖括号(<>)来明确地传入类型;编译器可以查看myString的值,然后把T设置为它的类型。 类型推论帮助我们保持代码精简和高可读性。如果编译器不能够自动地推断出类型的话,只能像上面那样明确的传入T的类型,在一些复杂的情况下,这是可能出现的。

15.3 使用泛型变量

使用泛型创建像identity这样的泛型函数时,编译器要求你在函数体必须正确的使用这个通用的类型。 换句话说,你必须把这些参数当做是任意或所有类型。

看下之前identity例子:

function identity<T>(arg: T): T {
    return arg;
}

如果我们想同时打印出arg的长度。 我们很可能会这样做:

function loggingIdentity<T>(arg: T): T {
    console.log(arg.length);  // Error: T doesn't have .length
    return arg;
}

如果这么做,编译器会报错说我们使用了arg.length属性,但是没有地方指明arg具有这个属性。 记住,这些类型变量代表的是任意类型,所以使用这个函数的人可能传入的是个数字,而数字是没有 .length属性的。

现在假设我们想操作T类型的数组而不直接是T。由于我们操作的是数组,所以.length属性是应该存在的。 我们可以像创建其它数组一样创建这个数组:

function loggingIdentity<T>(arg: T[]): T[] {
    console.log(arg.length);  // Array has a .length, so no more error
    return arg;
}

你可以这样理解loggingIdentity的类型:泛型函数loggingIdentity,接收类型参数T和参数arg,它是个元素类型是T的数组,并返回元素类型是T的数组。 如果我们传入数字数组,将返回一个数字数组,因为此时 T的的类型为number。 这可以让我们把泛型变量T当做类型的一部分使用,而不是整个类型,增加了灵活性。

我们也可以这样实现上面的例子:

function loggingIdentity<T>(arg: Array<T>): Array<T> {
    console.log(arg.length);  // Array has a .length, so no more error
    return arg;
}

15.4 泛型类型

泛型函数的类型与非泛型函数的类型没什么不同,只是有一个类型参数在最前面,像函数声明一样:

function identity<T>(arg: T): T {
    return arg;
}

let myIdentity: <T>(arg: T) => T = identity;

我们也可以使用不同的泛型参数名,只要在数量上和使用方式上能对应上就可以。

function identity<T>(arg: T): T {
    return arg;
}

let myIdentity: <U>(arg: U) => U = identity;

泛型接口了:

interface GenericIdentityFn {
    <T>(arg: T): T;
}

function identity<T>(arg: T): T {
    return arg;
}

let myIdentity: GenericIdentityFn = identity;

15.5 泛型类

泛型类看上去与泛型接口差不多。 泛型类使用( <>)括起泛型类型,跟在类名后面。

class GenericNumber<T> {
    zeroValue: T;
    add: (x: T, y: T) => T;
}

let myGenericNumber = new GenericNumber<number>();
myGenericNumber.zeroValue = 0;
myGenericNumber.add = function(x, y) { return x + y; };

GenericNumber类的使用是十分直观的,并且你可能已经注意到了,没有什么去限制它只能使用number类型。 也可以使用字符串或其它更复杂的类型。

let stringNumeric = new GenericNumber<string>();
stringNumeric.zeroValue = "";
stringNumeric.add = function(x, y) { return x + y; };

console.log(stringNumeric.add(stringNumeric.zeroValue, "test"));

与接口一样,直接把泛型类型放在类后面,可以帮助我们确认类的所有属性都在使用相同的类型。

类有两部分:静态部分和实例部分。 泛型类指的是实例部分的类型,所以类的静态属性不能使用这个泛型类型。

15.6 泛型约束

我们想用个函数输出参数的长度.length,为此,我们定义一个接口来描述约束条件。 创建一个包含 .length属性的接口,使用这个接口和extends关键字来实现约束:

interface Lengthwise {
    length: number;
}

function loggingIdentity<T extends Lengthwise>(arg: T): T {
    console.log(arg.length);  // Now we know it has a .length property, so no more error
    return arg;
}

现在这个泛型函数被定义了约束,因此它不再是适用于任意类型:

loggingIdentity(3);  // Error, number doesn't have a .length property

我们需要传入符合约束类型的值,必须包含必须的属性:

loggingIdentity({length: 10, value: 3});

在泛型约束中使用类型参数

你可以声明一个类型参数,且它被另一个类型参数所约束。 比如,现在我们想要用属性名从对象里获取这个属性。 并且我们想要确保这个属性存在于对象 obj上,因此我们需要在这两个类型之间使用约束。

function getProperty(obj: T, key: K) {
    return obj[key];
}

let x = { a: 1, b: 2, c: 3, d: 4 };

getProperty(x, "a"); // okay
getProperty(x, "m"); // error: Argument of type 'm' isn't assignable to 'a' | 'b' | 'c' | 'd'.

在泛型里使用类类型:在TypeScript使用泛型创建工厂函数时,需要引用构造函数的类类型。比如,

function create<T>(c: {new(): T; }): T {
    return new c();
}

一个更高级的例子,使用原型属性推断并约束构造函数与类实例的关系。

class BeeKeeper {
    hasMask: boolean;
}

class ZooKeeper {
    nametag: string;
}

class Animal {
    numLegs: number;
}

class Bee extends Animal {
    keeper: BeeKeeper;
}

class Lion extends Animal {
    keeper: ZooKeeper;
}

function createInstance<A extends Animal>(c: new () => A): A {
    return new c();
}

createInstance(Lion).keeper.nametag;  // typechecks!
createInstance(Bee).keeper.hasMask;   // typechecks!

十六、枚举

16.1 引言

使用枚举我们可以定义一些带名字的常量。 使用枚举可以清晰地表达意图或创建一组有区别的用例。 TypeScript支持数字的和基于字符串的枚举

16.2 数字枚举

enum Direction {
    Up = 1,
    Down,
    Left,
    Right
}

如上,我们定义了一个数字枚举, Up使用初始化为 1。 其余的成员会从 1开始自动增长。 换句话说,Direction.Up的值为 1Down2Left3Right4

我们还可以完全不使用初始化器:

enum Direction {
    Up,
    Down,
    Left,
    Right,
}

现在, Up的值为 0Down的值为 1等等。 当我们不在乎成员的值的时候,这种自增长的行为是很有用处的,但是要注意每个枚举成员的值都是不同的。

使用枚举很简单:通过枚举的属性来访问枚举成员,和枚举的名字来访问枚举类型:

enum Response {
    No = 0,
    Yes = 1,
}

function respond(recipient: string, message: Response): void {
    // ...
}

respond("Princess Caroline", Response.Yes)

数字枚举可以被混入到计算过的和常量成员。 简短地说,不带初始化器的枚举或者被放在第一的位置,或者被放在使用了数字常量或其它常量初始化了的枚举后面。 换句话说,下面的情况是不被允许的:

enum E {
    A = getSomeValue(),
    B, // error! 'A' is not constant-initialized, so 'B' needs an initializer
}

16.3 字符串枚举

字符串枚举的概念很简单,但是有细微的运行时的差别。 在一个字符串枚举里,每个成员都必须用字符串字面量,或另外一个字符串枚举成员进行初始化。

enum Direction {
    Up = "UP",
    Down = "DOWN",
    Left = "LEFT",
    Right = "RIGHT",
}

由于字符串枚举没有自增长的行为,字符串枚举可以很好的序列化。 换句话说,如果你正在调试并且必须要读一个数字枚举的运行时的值,这个值通常是很难读的 - 它并不能表达有用的信息(尽管反向映射会有所帮助),字符串枚举允许你提供一个运行时有意义的并且可读的值,独立于枚举成员的名字。

16.4 异构枚举

从技术的角度来说,枚举可以混合字符串和数字成员,但是似乎你并不会这么做:

enum BooleanLikeHeterogeneousEnum {
    No = 0,
    Yes = "YES",
}

除非你真的想要利用JavaScript运行时的行为,否则我们不建议这样做。

16.5 计算的和常量成员

每个枚举成员都带有一个值,它可以是 常量计算出来的。 当满足如下条件时,枚举成员被当作是常量:

  • 它是枚举的第一个成员且没有初始化器,这种情况下它被赋予值 0
// E.X is constant:
enum E { X }
  • 它不带有初始化器且它之前的枚举成员是一个 数字常量。 这种情况下,当前枚举成员的值为它上一个枚举成员的值加1。
// All enum members in 'E1' and 'E2' are constant.

enum E1 { X, Y, Z }

enum E2 {
    A = 1, B, C
}
  • 枚举成员使用 常量枚举表达式初始化。 常数枚举表达式是TypeScript表达式的子集,它可以在编译阶段求值。 当一个表达式满足下面条件之一时,它就是一个常量枚举表达式:
    • 一个枚举表达式字面量(主要是字符串字面量或数字字面量)
    • 一个对之前定义的常量枚举成员的引用(可以是在不同的枚举类型中定义的)
    • 带括号的常量枚举表达式
    • 一元运算符 +, -, ~其中之一应用在了常量枚举表达式
    • 常量枚举表达式做为二元运算符 +, -, *, /, %, <<, >>, >>>, &, |, ^的操作对象。 若常数枚举表达式求值后为 NaNInfinity,则会在编译阶段报错。

所有其它情况的枚举成员被当作是需要计算得出的值。

enum FileAccess {
    // constant members
    None,
    Read    = 1 << 1,
    Write   = 1 << 2,
    ReadWrite  = Read | Write,
    // computed member
    G = "123".length
}

16.6 反向映射

除了创建一个以属性名做为对象成员的对象之外,数字枚举成员还具有了 反向映射,从枚举值到枚举名字。 例如,在下面的例子中:

enum Enum {
    A
}
let a = Enum.A;
let nameOfA = Enum[a]; // "A"

TypeScript可能会将这段代码编译为下面的JavaScript:

var Enum;
(function (Enum) {
    Enum[Enum["A"] = 0] = "A";
})(Enum || (Enum = {}));
var a = Enum.A;
var nameOfA = Enum[a]; // "A"

生成的代码中,枚举类型被编译成一个对象,它包含了正向映射( name -> value)和反向映射( value ->name)。 引用枚举成员总会生成为对属性访问并且永远也不会内联代码。

要注意的是 不会为字符串枚举成员生成反向映射。

16.7 const枚举

大多数情况下,枚举是十分有效的方案。 然而在某些情况下需求很严格。 为了避免在额外生成的代码上的开销和额外的非直接的对枚举成员的访问,我们可以使用 const枚举。 常量枚举通过在枚举上使用 const修饰符来定义。

const enum Enum {
    A = 1,
    B = A * 2
}

常量枚举只能使用常量枚举表达式,并且不同于常规的枚举,它们在编译阶段会被删除。 常量枚举成员在使用的地方会被内联进来。 之所以可以这么做是因为,常量枚举不允许包含计算成员。

const enum Directions {
    Up,
    Down,
    Left,
    Right
}

let directions = [Directions.Up, Directions.Down, Directions.Left, Directions.Right]

生成后的代码为:

var directions = [0 /* Up */, 1 /* Down */, 2 /* Left */, 3 /* Right */];

16.8 外部枚举

外部枚举用来描述已经存在的枚举类型的形状。

declare enum Enum {
    A = 1,
    B,
    C = 2
}

外部枚举和非外部枚举之间有一个重要的区别,在正常的枚举里,没有初始化方法的成员被当成常数成员。 对于非常数的外部枚举而言,没有初始化方法时被当做需要经过计算的。