TypeScript 介绍

TypeScript-官方中文文档

TypeScript是微软开发的开源编程语言,是 JavaScript 的超集,是开发大型应用的基石,提供了更丰富的语法提示,在编写阶段能够检查错误

TypeScript 可处理已有的 JavaScript 代码,并只对其中的 TypeScript 代码进行编译
TypeScript 是一种面向对象的编程语言(对象和类)

  1. 安装 TypeScript,会提供一个 tsc 工具执行相应命令
  • tsc -h 查看这个工具有哪些命令
  1. 之后新建一个 ts 文件,执行 tsc ts文件名 命令将 TypeScript 转换为 JavaScript 代码,当前目录下就会生成一个与 ts 同名的 js 文件,使用 node js文件名 来执行 js。
    (可以同时编译多个 ts 文件:tsc file1.ts file2.ts file3.ts)
    (如果 ts 代码修改后,要重新编译,不用删除之前生成的 js 文件,会自动更新)

分号在 TypeScript 中是可选的,是否使用可以看项目的代码风格; 几条语句在同一行必须使用分号,不然会报错

TypeScript 中的数据类型:

TypeScript 里的类型注解是一种轻量级的为函数或变量添加约束的方式

回顾 JS 中的数据类型

  • 原始数据类型:boolean string null undefined number symbol

  • 引用数据类型:object

    TS 中的数据类型

  • boolean:最基本的数据类型就是简单的 true / false 值

  • number: 数字类型。双精度 64 位浮点值。它可以用来表示整数和分数,除了支持十进制、十六进制字面量,还支持 ES6 引入的二进制、八进制字面量,ts 代码编译为 js 代码,二进制与八进制数会转换为十进制

  • string: 字符串类型。一个字符系列,使用单引号(’)或双引号(”)来表示字符串类型。模板字符串来定义多行文本和内嵌表达式,以反引号(`)包围,并以 ${ expr } 这种形式嵌入表达式

  • 数组:声明变量为数组,两种方式定义数组:

    • ① 可以在元素类型后面接上 [],表示由此类型元素组成的一个数组: let arr: number[] = [1, 2];
    • ② 使用数组泛型,*Array<元素类型T>*:let arr: Array<number> = [1, 2];
    • ③ 用接口 interface:
      1
      2
      3
      4
      interface StringArray {
      [index: number]: string;
      }
      let namesArray: StringArray = ["白醭飚尘", "局外人", "E'tranger"]
  • 元组 tuple:元组类型允许表示一个已知元素数量和类型的数组,各元素的类型不必相同,对应位置的类型需要相同。(当访问一个元组中越界的元素,会使用联合类型替代)

    1
    2
    3
    4
    let x: [string, number];
    x = ['白醭飙尘', 1]; // 运行正常
    x = [1, '白醭飙尘']; // 报错
    console.log(x[0]); // 输出 白醭飙尘
  • 枚举 enum:枚举类型是对 JavaScript 标准数据类型的一个补充。 像 C# 等其它语言一样,使用枚举类型可以为一组数值赋予友好的名字
    默认情况下,从 0 开始为元素编号

    1
    2
    3
    enum Color {Orange, Blue, Yellow, Red}
    let c : Color = Color.red
    console.log(c); // 3,编号才能够 0 开始,四个元素

    也可以手动指定[全部]成员的数值。枚举类型提供的一个便利是可以由枚举的值得到它的名字

    1
    2
    3
    4
    5
    6
    // 知道数值为 2,但是不确定它映射到 Color里的哪个名字,可以查找相应的名字:
    enum Color {Orange = 1, Blue = 5, Yellow, Red}
    let colorName: string = Color[5];

    console.log(colorName); // 显示 'Blue',因为上面代码里它的值是 5
    console.log(Color[6]); // Yellow
  • any: 任意类型。声明为 any 的变量可以赋予任意类型的值。使用场景:

    • 1、变量的值会动态改变时,比如来自用户的输入或第三方代码库,任意值类型可以让这些变量跳过编译阶段的类型检查
    • 2、改写现有代码时,any 类型是十分有用的,它允许在编译时可选择地包含或移除类型检查
    • 3、定义存储各种类型数据的数组时
  • void:表示没有任何类型,某种程度上像是与 any 相反
    用于标识方法返回值的类型,表示该方法没有返回值

    1
    2
    3
    getName():void { 
    console.log("白醭飙尘")
    }

    声明一个 void 类型的变量没有什么大用,因为你只能为它赋予 undefined 和 null

    1
    let unusable: void = undefined
  • nullundefined:null 类型只有 null 一个值,表示对象值缺失,对象空引用; undefined 用于初始化变量为一个未定义的值默认情况下 null 和 undefined 是所有类型的子类型,就是说可以把 null 和 undefined 赋值给 number 类型的变量。然而,当指定了 –strictNullChecks 标记,null 和 undefined 只能赋值给 void 和它们各自。也许在某处想传入一个 string 或 null 或 undefined,可以使用联合类型 string | null | undefined

  • never:表示的是那些永不存在的值的类型,例如那些总是会抛出异常或无法执行到终止点(例如无限循环)或根本就不会有返回值的函数表达式或箭头函数表达式的返回值类型; 变量也可能是 never 类型,当它们被永不为真的类型保护所约束时
    never 类型是任何类型的子类型,也可以赋值给任何类型;然而,没有类型是 never 的子类型或可以赋值给 never 类型(除了 never 本身之外)
    一些返回 never 类型的函数:

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    // 返回 never 的函数必须存在无法达到的终点
    function error(message: string): never {
    throw new Error(message);
    }
    function infiniteLoop(): never {
    while (true) {
    }
    }

    // 推断的返回值类型为 never
    function fail() {
    return error("Something failed");
    }
  • object:表示非原始类型,也就是除 number,string,boolean,symbol,null 或 undefined 之外的类型使用 object 类型,就可以更好的表示像 Object.create 这样的 API,例如:

    1
    2
    3
    4
    5
    6
    7
    8
    9
    declare function create(o: object | null): void;

    create({ prop: 0 }); // OK
    create(null); // OK

    create(42); // Error
    create("string"); // Error
    create(false); // Error
    create(undefined); // Error

    类型断言

开发者有时候对某些值比 TS 更了解详细信息,清楚知道一个实体具有比它现有类型更确切的类型,此时通过类型断言告诉编辑器,“相信我,我知道自己在干什么”。类型断言好比其它语言里的类型转换,但不进行特殊的数据检查和解构。它没有运行时的影响,只是在编译阶段起作用。TypeScript 会假设开发者已经进行了必须的检查

类型断言有两种等价形式:

  • “尖括号”语法
    1
    2
    let someValue: any = "this is a string";
    let strLength: number = (<string>someValue).length;
  • as 语法
    1
    2
    let someValue: any = "this is a string";
    let strLength: number = (someValue as string).length;

    当在 TS 中使用 JSX 时,只有 as 语法断言被允许。

一些注意的点

  • ```javascript
    const getValue = () => {
    return 0
    }

    enum List {
    A = getValue(),
    B = 2, // 此处必须要初始化值,不然编译不通过
    C
    }
    console.log(List.A) // 0
    console.log(List.B) // 2
    console.log(List.C) // 3

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18

    A 的值是被计算出来的。注意注释部分,如果某个属性的值是计算出来的,那么它后面一位的成员必须要初始化值。

    - 全局声明变量,变量名不要使用 name,否则会与 DOM 中的全局 window 对象下的 name 属性出现重名

    - 短路运算符(&& 与 ||)
    && 前面的表达式为 false 时,其后面的表达式就不会再执行了
    同理,|| 前面的表达式已经为 true 时,其后面的表达式也不会再执行

    - instanceof 运算符用于判断对象是否为指定的类型

    - for...in 语句用于一组值的集合或列表进行迭代输出

    ```javascript
    for (let val in list) {
    //语句
    }
    // val 需要为 string 类型或 any 类型
  • for…of 语句创建一个循环来迭代可迭代的对象,允许遍历 Arrays(数组), Strings(字符串), Maps(映射), Sets(集合)等可迭代的数据结构等

函数

1、函数定义如下:

  • return_type 是返回值的类型。
  • return 关键词后跟着要返回的结果。
  • 一般情况下,一个函数只有一个 return 语句。
  • 返回值的类型需要与函数定义的返回类型(return_type)一致。
    1
    2
    3
    4
        function function_name():return_type { 
    // 语句
    return value;
    }

2、可以向函数发送多个参数,每个参数使用逗号 , 分隔

1
2
function func_name( param1 [:datatype], param2 [:datatype]) {   // [表示可设置或不设置]
}

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

1
2
3
4
5
6
7
8
9
10
function buildName(firstName: string, lastName?: string) { // 共享同样的类型(firstName: string, lastName?: string) => string
if (lastName)
return firstName + " " + lastName;
else
return firstName;
}

let result1 = buildName("Bob"); // 正确
let result2 = buildName("Bob", "Adams", "Sr."); // 错误,参数太多了
let result3 = buildName("Bob", "Adams"); // 正确

可选参数必须跟在必须参数后面

4、可以设置参数的默认值,不能同时给参数设置默认值和可选(设了默认值的参数就是可选的)。在所有必须参数后面的带默认初始化的参数都是可选的,与可选参数一样,在调用函数的时候可以省略。也就是说可选参数与末尾的默认参数共享参数类型

1
function function_name(param1[:type],param2[:type] = default_value) { }

与普通可选参数不同的是,带默认值的参数不需要放在必须参数的后面

如果带默认值的参数出现在必须参数前面,用户必须明确的传入 undefined值来获得默认值

1
2
3
4
function buildName(firstName = "Will", lastName: string) {
return firstName + " " + lastName;
}
let result = buildName(undefined, "Adams"); // okay and returns "Will Adams"

5、剩余参数。调用函数时不知道要传入多少参数,剩余参数语法允许将不确定数量的参数作为一个数组传入

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

剩余参数会被当做个数不限的可选参数。 可以一个都没有,同样也可以有任意个

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

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

1
const res = new Function ([arg1[, arg2[, ...argN]],] functionBody)

functionBody:一个含有包括函数定义的 JavaScript 语句的字符串

举例:

1
2
3
const myFunction = new Function("a", "b", "return a * b")
let x = myFunction(4, 3)
console.log(x) // 12

7、匿名、递归、函数表达式与 JS 一样,接下来讲 Lambda 函数,也就是箭头函数

1
2
3
4
5
const foo = ( x: number )=> {        
x = 10 + x
console.log(x)
}
foo(100)

在 JS 中像下面这样判断一个参数的类型,麻烦,而 TS 只要设置参数类型就可以了

1
2
3
4
5
6
7
const func = (x)=> {     
if(typeof x == "number") {
console.log(x + " 是一个数字")
} else if(typeof x == "string") {
console.log(x + " 是一个字符串")
}
}

8、函数的类型注解方式,函数类型分为参数类型和返回值类型

  • 函数声明(命名函数 Named function)
    1
    2
    3
    function test(a: number, b: number): number { // 注解参数和返回值类型,若没有返回值用 void
    return a + b;
    }
  • 函数表达式(匿名函数 Anonymous function)
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    // 像函数声明那样注解是可以的
    let test1 = function (a: number, b: number): number {
    return a + b;
    }

    // 推荐函数表达式用下面这种注解,以参数列表的形式写出参数类型,为每个参数指定一个名字和类型,名字只是为了增加可读性
    // 只要参数类型是匹配的,那么就认为它是有效的函数类型,而不在乎参数名是否正确
    let test1: (baseValue: number, increment: number) => number = function (a, b) { // => number 代码返回值类型,不是 es6 中的箭头函数
    return a + b;
    }
    let test2: (baseValue: string) => {} = function (a) { // => {} 表示返回值是一个对象
    return {name: a}
    }
    console.log(test2("白菜")); // { name: '白菜' }

数组

数组解构,把数组元素赋值给变量

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
let arr: number[] = [12, 13] 
let [x, y] = arr // 将数组的两个元素赋值给变量 x 和 y
console.log(x)
console.log(y)

[first, second] = [second, first] // swap variables

function f([first, second]: [number, number]) { // 用于函数参数
console.log(first);
console.log(second);
}
f(arr);

let [first, ...rest] = [1, 2, 3, 4]; // 在数组里使用...语法创建剩余变量
console.log(first); // outputs 1 因为是 JavaScript,可以忽略不关心的尾随元素
console.log(rest); // outputs [ 2, 3, 4 ]
let [, second, , fourth] = [1, 2, 3, 4]; // 忽略其它元素

push() 向数组的末尾添加一个或更多元素,并返回新的长度

reduce() 将数组元素计算为一个值(从左到右)

1
2
let total = [0, 1, 2, 3].reduce((a, b) => a + b)
console.log("total is : " + total ) // 6

TypeScript Map 对象

Map 对象保存键值对,并且能够记住键的原始插入顺序。任何值(对象或者原始值) 都可以作为一个键或一个值。

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

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

map.keys() - 返回一个 Iterator 对象,包含了 Map 对象中每个元素的键
map.values() – 返回一个新的 Iterator 对象,包含了Map对象中每个元素的值

TS 中使用 for…of 迭代 Map

TypeScript 联合类型

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

创建联合类型的语法格式:Type1 | Type2 | Type3

联合类型数组:let arr: number[] | string[]

接口 interface

接口是一系列抽象方法的声明,是一些方法特征的集合,这些方法都应该是抽象的,需要由具体的类去实现,然后第三方就可以通过这组抽象方法调用,让具体的类执行具体的方法,接口定义:interface interface_name { }

接口不能转换为 JS

可选属性:*?*
任意属性: [propName: string]: any;
只读属性: readonly

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

1
2
3
interface arraylist {    
[index: number]:string
}

接口继承就是说接口可以通过其他接口来扩展自己。Typescript 允许接口继承多个接口。继承使用关键字 extends。
接口可以用来描述对象的结构,每个属性是什么类型,还能实现类型抽象

多接口继承语法格式:
Child_interface_name extends super_interface1_name, super_interface2_name,…,super_interfaceN_name

单继承实例:

1
2
3
4
5
6
7
8
9
10
interface Person {    
age:number
}
interface Musician extends Person {
instrument:string
}
let drummer = <Musician>{};
drummer.age = 27
drummer.instrument = "Drums"
console.log("年龄: " + drummer.age)

类(OOP 面向对象的三大特性:封装、继承、多态)

类继承使用关键字 extends,子类除了不能继承父类的私有成员(方法和属性)和构造函数,其他的都可以继承
TypeScript 子类只能继承一个父类,不支持继承多个类,但 TypeScript 支持多重继承(A 继承 B,B 继承 C)

语法格式:class child_class_name extends parent_class_name

类继承后,子类可以对父类的方法重新定义,这个过程称之为方法的重写
其中 super 关键字是对父类的直接引用,该关键字可以引用父类的属性和方法

1
class PrinterClass {    doPrint():void {      console.log("父类的 doPrint() 方法。")    } }  class StringPrinter extends PrinterClass {    doPrint():void {       super.doPrint() // 调用父类的函数      console.log("子类的 doPrint()方法。")   } }

static 关键字用于定义类的数据成员(属性和方法)为静态的,静态成员可以直接通过类名调用

访问控制修饰符:TypeScript 中,可以使用访问控制符来保护对类、变量、方法和构造方法的访问。TypeScript 支持 3 种不同的访问权限

  • public(默认) : 公有,可以在任何地方被访问
  • protected : 受保护,可以被其自身以及其子类和父类访问
  • private : 私有,只能被其定义所在的类访问

类可以实现接口,使用关键字 implements,并将 interest 字段作为类的属性使用

1
interface ILoan {    interest:number }  class AgriLoan implements ILoan {    interest:number    rebate:number       constructor(interest:number,rebate:number) {       this.interest = interest       this.rebate = rebate    } }  let obj = new AgriLoan(10,1) console.log("利润为 : " + obj.interest + ",抽成为 : " + obj.rebate)

对象

对象解构

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
let o = {
a: "foo",
b: 12,
c: "bar"
}
let { a, b } = o // 通过 o.a and o.b 创建了 a 和 b,如果不需要忽略 c
console.log(a, b) // foo 12

// 就像数组解构,可以用没有声明的赋值,需要用括号将它括起来,因为 JS 通常会将以 { 起始的语句解析为一个块
({ a, b } = { a: "baz", b: 101 }) // baz 101

// 可以在对象里使用...语法创建剩余变量
let { a, ...passthrough } = o
console.log(passthrough) // {b: 12, c: 'bar'}
let total = passthrough.b + passthrough.c.length // 15

let { a: newName1, b: newName2 } = o // 属性重命名。这里不是类型注解,而是给属性以不同的名字,将 a: newName1 读做 "a 作为 newName1"

// 默认值可以在属性为 undefined 时使用缺省值 ("?"表示参数 b 是可传可不传的)
function keepWholeObject(wholeObject: { a: string, b?: number }) {
let { a, b = 1001 } = wholeObject;
}
// 现在,即使 b 为 undefined,keepWholeObject 函数的变量 wholeObject 的属性 a 和 b 都会有值

展开操作符正与解构相反。它允许你将一个数组展开为另一个数组,或将一个对象展开为另一个对象
对象的展开比数组的展开要复杂的多。像数组展开一样,它是从左至右进行处理,但结果仍为对象。 这就意味着出现在展开对象后面的属性会覆盖前面的属性

对象展开还有其它一些意想不到的限制:

  • 首先,它仅包含对象 自身的可枚举属性
  • 其次,TypeScript 编译器不允许展开泛型函数上的类型参数

类型模板,如果定义了一个对象 let sites = {site: 0}; 然后想在对象中添加方法 sites.sayHello = function(){ return "hello";},在 TS 中这种方式编译时会报错,因为 TS 中的对象必须是特定类型的实例

1
2
3
4
5
6
7
8
9
let sites = { 
site1: "Runoob",
site2: "Google",
sayHello: function () { } // 类型模板
};
sites.sayHello = function () {
console.log("hello " + sites.site1);
};
sites.sayHello();

鸭子类型(Duck Typing) 是动态类型的一种风格,是多态(Polymorphism)的一种形式。

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

在鸭子类型中,关注点在于对象的行为,能作什么;而不是关注对象所属的类型

命名空间

namespace 解决重名问题,如果要在外部可以调用 SomeNameSpaceName 中的类和接口,则需要在类和接口前面添加 export 关键字

1
2
3
4
namespace SomeNameSpaceName { 
export interface ISomeInterfaceName { }
export class SomeClassName { }
}
  • 要在另外一个命名空间调用,语法:SomeNameSpaceName.SomeClassName;
  • 若一个命名空间在一个单独的 TS 文件中,则应使用三道斜杠 /// 引用:
    /// <reference path = "SomeFileName.ts" />

命名空间支持嵌套

模块

TypeScript 模块的设计理念是可以更换的组织代码。

模块是在其自身的作用域里执行,并不是在全局作用域,这意味着定义在模块里面的变量、函数和类等在模块外部是不可见的,除非明确地使用 export 导出它们。类似地,我们必须通过 import 导入其他模块导出的变量、函数、类等

两个模块之间的关系是通过在文件级别上使用 import 和 export 建立的

模块使用模块加载器去导入其它的模块。 在运行时,模块加载器的作用是在执行此模块代码前去查找并执行这个模块的所有依赖。 大家最熟知的JavaScript模块加载器是服务于 Node.js 的 CommonJS 和服务于 Web 应用的 Require.js,此外还有有 SystemJs 和 Webpack

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

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

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

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

声明文件

TS 是 JS 的超集,开发过程中不可避免要使用 JS 的第三方库

通过直接引用可以调用库的类和方法,但是无法使用 TypeScript 诸如类型检查等特性功能

为了解决这个问题,需要将这些库里的函数和方法体去掉后只保留导出类型声明,而产生了一个描述 JavaScript 库和模块信息的声明文件

通过引用这个声明文件,就可以借用 TypeScript 的各种特性来使用库文件了

声明文件以 .d.ts 为后缀
声明文件或模块的语法格式:declare module Module_Name { }
TypeScript 引入声明文件语法格式:/// <reference path = " runoob.d.ts" />

当然,很多流行的第三方库的声明文件不需要自己定义了,有人已经定义好了

不经过 tsc 命令直接运行 ts 代码

首先在全局下安装,打开按 Ctrl + R,输入 cmd,并选择以管理员身份运行 dos 窗口
npm install -g typescript
npm install -g ts-node

这样用 VSCode 打开编辑器 ts-node ts文件名 命令运行可能还是会报错

1
2
3
4
5
6
7
8
9
D:\software\Node.js\node_global\node_modules\ts-node\src\index.ts:692
return new TSError(diagnosticText, diagnosticCodes);
^
TSError: ⨯ Unable to compile TypeScript:
script.ts:3:1 - error TS2584: Cannot find name 'console'. Do you need to change your target library? Try changing the 'lib' compiler option to include 'dom'.

3 console.log(myName)
~~~~~~~
......

此时可以在 ts 文件的当前目录下打开终端运行命令 npm install -D tslib @types/node 多安装一个依赖包。再运行命令 ts-node ts文件名 就可以了

断点调试

借助断点调试,观察代码的执行过程
断点(Breakpoint): 程序暂停的位置(调试时,程序运行到此处,就会暂停)

如下图,左边是打的断点,右边是程序运行到断点处,程序暂停

暂停之后会有工具栏可以控制程序代码一行行执行

在 VSCode 中,调试 TS 代码是需要配置的

  • 配置步骤:
    一 准备要调试的 TS 文件
    二 添加调试配置
    • 1 打开调试窗口:点击编辑器左侧活动栏像虫子一样的 Debug 按钮
    • 2 生成默认配置,点击 Debug 后面的下拉框,选择添加配置
    • 3 修改配置内容如下:
      1
      2
      3
      4
      5
      6
      7
      8
      9
      10
      11
      12
      13
      14
      15
      16
      17
      18
      19
      20
      21
      {
      // 使用 IntelliSense 了解相关属性
      // 悬停以查看现有属性的描述
      //
      {
      "version": "0.2.0",
      "configurations": [
      {
      "type": "node",
      "request": "launch",
      "name": "运行或调试当前文件",
      "program": "${workspaceFolder}/${relativeFile}",
      "preLaunchTask": "tsc: 监视 - tsconfig.json", //VSCode界面使用中文语言的话用这个
      // "preLaunchTask": "tsc: watch - tsconfig.json", //VSCode界面使用英文语言的话用这个
      "outFiles": [
      "${workspaceFolder}/build/es5/**/*.js"
      ]
      }
      ]
      }
      }
      三 安装调试用到的包,就是前面安装的 ts-node 依赖包
  • 调试代码步骤:先在 ts/js 文件中想调试的代码行号前面点亮红点,再点击编辑器左侧带虫子样式的菜单即可开启调试。

报错

  • 在 HTML 文件中引入 ts 文件,并在浏览器中打开页面报错:Refused to execute script from ‘http://127.0.0.1:5500/script.ts' because its MIME type (‘video/mp2t’) is not executable.