TypeScript 是由微软开发的开源的编程语言。它是javascript的一个超集,而且本质上向这个语言添加了可选的静态类型和基于类的面向对象的编程。
TypeScript 提供最新的和不断发展的 JavaScript 特性,包括那些来自 2015 年的 ECMAScript 和未来的提案中的特性,比如异步功能和 Decorators,以帮助建立健壮的组件。下图显示了 TypeScript 与 ES5、ES2015 和 ES2016 之间的关系:
1.1 TypeScript 与 javaScript的区别
TypeScript | javaScript |
---|---|
它是javaScript的超集,用于解决大型项目的代码复杂性 | 一种脚本语言,用于创建动态网页 |
可以在编译期间发现并纠正错误 | 作为一种解释型语言,只能在运行时发现错误 |
强类型,支持静态和动态类型 | 若类型,没有静态类型选项 |
最终被编译成JavaScript代码,使浏览器可以理解 | 可以直接在浏览器中使用 |
支持模块、范型和接口 | 不支持模块,范型和接口 |
支持ES3,ES4,ES5和ES6等 | 不支持编译其他ES3,ES4,ES5和ES6功能 |
社区的支持仍在增长,而且不是很大 | 大量的社区支持以及文档和解决问题的支持 |
1.2获取TypeScript
1)安装 TypeScript
npm install -g typescript
2)编译TypeScript
可以在vscode 的终端,输入命令, tsc helloWorld.ts
默认情况下会在当前文件所在目录下生成同名的 js
文件,helloWorld.ts=>helloWorld.js
outDir,指定编译文件输出目录
如果说上面生成的ts 文件是默认的,那么,想把生成的js 文件放到一个指定的文件夹,可以执行如下命令
tsc --outDir ./dist ./src/helloWorld.ts
``
**target,指定编译的代码版本目标,默认为ES3**
tsc --outDir ./dist --target ES6 ./src/helloWorld.ts
``
watch,在监听模式下运行,当文件发生改变的时候,自动编译
tsc --outDir ./dist --target ES6 --watch ./src/helloWorld.ts
通过上面几个命令,可以编译ts,问题就是每次编译都输入一大堆的选项,很繁琐,其实,我们给以写个配置文件,tsconfig.json,可以把上面的编译选项保存到这个配置文件中。
编译配置文件
我们可以把编译的一些选项保存在一个指定的 json
文件中,默认情况下 tsc
命令运行的时候会自动去加载运行命令所在的目录下的 tsconfig.json
文件,配置文件格式如下:
{
"compilerOptions": {
"outDir": "./dist",
"target": "ES2015",
"watch": true,
},
// ** : 所有目录(包括子目录)
// * : 所有文件,也可以指定类型 *.ts
"include": ["./src/**/*"]
}
有了单独的配置文件,我们就可以直接运行命令
tsc
指定加载的配置文件
使用 --project
或 -p
指定配置文件目录,会默认加载该目录下的 tsconfig.json
文件
tsc -p ./configs
也可以指定某个具体的配置文件
tsc -p ./configs/ts.json
2.1 Boolean 类型
let isDone: boolean = false;
// ES5:var isDone = false;
2.2 Number 类型
let count: number = 10;
// ES5:var count = 10;
2.3 String 类型
let name: string = "Semliker";
// ES5:var name = 'Semlinker';
2.4 空和未定义类型
因为在 Null
和 Undefined
这两种类型有且只有一个值,在标注一个变量为 Null
和 Undefined
类型,那就表示该变量不能修改了
let a: null;
// ok
a = null;
// error
a = 1;
默认情况下 null
和 undefined
是所有类型的子类型。 就是说你可以把 null
和 undefined
其它类型的变量
let a: number;
// ok
a = null;
如果一个变量声明了,但是未赋值,那么该变量的值为 undefined
,但是如果它同时也没有标注类型的话,默认类型为 any
内置对象类型
在 JavaScript
中,有许多的内置对象,比如:Object、Array、Date……,我们可以通过对象的 构造函数或者类来进行标注
let a: object = {};
// 数组这里标注格式有点不太一样,后面我们在数组标注中进行详细讲解
let arr: Array = [1,2,3];
let d1: Date = new Date();
自定义对象类型
另外一种情况,许多时候,我们可能需要自定义结构的对象。这个时候,我们可以:
let a: {username: string; age: number} = {
username: 'zMouse',
age: 35
};
// ok
a.username;
a.age;
// error
a.gender;
优点
: 方便、直接
缺点
: 不利于复用和维护
接口:
// 这里使用了 interface 关键字,在后面的接口章节中会详细讲解
interface Person {
username: string;
age: number;
};
let a: Person = {
username: 'zMouse',
age: 35
};
// ok
a.username;
a.age;
// error
a.gender;
优点
: 复用性高
缺点
: 接口只能作为类型标注使用,不能作为具体值,它只是一种抽象的结构定义,并不是实体,没有具体功能实现
类与构造函数:
// 类的具体使用,也会在后面的章节中讲解
class Person {
constructor(public username: string, public age: number) {
}
}
// ok
a.username;
a.age;
// error
a.gender;
优点
: 功能相对强大,定义实体的同时也定义了对应的类型
缺点
: 复杂,比如只想约束某个函数接收的参数结构,没有必要去定一个类,使用接口会更加简单
interface AjaxOptions {
url: string;
method: string;
}
function ajax(options: AjaxOptions) {}
ajax({
url: '',
method: 'get'
});
扩展
包装对象:
这里说的包装对象其实就是 JavaScript
中的 String
、Number
、Boolean
,我们知道 string
类型 和 String
类型并不一样,在 TypeScript
中也是一样
let a: string;
a = '1';
// error String有的,string不一定有(对象有的,基础类型不一定有)
a = new String('1');
let b: String;
b = new String('2');
// ok 和上面正好相反
b = '2';
数组类型
TypeScript
中数组存储的类型必须一致,所以在标注数组类型的时候,同时要标注数组中存储的数据类型
使用泛型标注
let arr1: Array = [];
// ok
arr1.push(100);
// error
arr1.push('ts');
简单标注
let arr2: string[] = [];
// ok
arr2.push('ts');
// error
arr2.push(1);
元组类似数组,但是存储的元素类型不必相同,但是需要注意:
let data1: [string, number] = ['ts', 100];
// ok
data1.push(100);
// ok
data1.push('100');
// error
data1.push(true);
枚举类型
枚举的作用组织收集一组关联数据的方式,通过枚举我们可以给一组有关联意义的数据赋予一些友好的名字
enum HTTP_CODE {
OK = 200,
NOT_FOUND = 404,
METHOD_NOT_ALLOWED
};
// 200
HTTP_CODE.OK;
// 405
HTTP_CODE.METHOD_NOT_ALLOWED;
// error
HTTP_CODE.OK = 1;
注意事项:
字符串类型枚举
枚举类型的值,也可以是字符串类型
enum URLS {
USER_REGISETER = '/user/register',
USER_LOGIN = '/user/login',
// 如果前一个枚举值类型为字符串,则后续枚举项必须手动赋值
INDEX = 0
}
注意:如果前一个枚举值类型为字符串,则后续枚举项必须手动赋值
小技巧:枚举名称可以是大写,也可以是小写,推荐使用全大写(通常使用全大写的命名方式来标注值为常量)
无值类型
表示没有任何数据的类型,通常用于标注无返回值函数的返回值类型,函数默认标注类型为:void
function fn():void {
// 没有 return 或者 return undefined
}
在 strictNullChecks
为 false
的情况下,undefined
和 null
都可以赋值给 void
,但是当 strictNullChecks
为 true
的情况下,只有 undefined
才可以赋值给 void
Never类型
当一个函数永远不可能执行 return
的时候,返回的就是 never
,与 void 不同,void
是执行了 return
, 只是没有值,never
是不会执行 return
,比如抛出错误,导致函数终止执行
function fn(): never {
throw new Error('error');
}
有的时候,我们并不确定这个值到底是什么类型或者不需要对该值进行类型检测,就可以标注为 any
类型
let a: any;
any
类型any
类型any
类型也可以赋值给任意类型any
类型有任意属性和方法any
类型,也意味着放弃对该值的类型检测,同时放弃 IDE 的智能提示小技巧:当指定
noImplicitAny
配置为true
,当函数参数出现隐含的any
类型时报错
未知类型
unknow,3.0 版本中新增,属于安全版的 any,但是与 any 不同的是:
function add(x: number, y: number): number {
return x + y;
}
三、TypeScript 断言
有时候你会遇到这样的情况,你会比 TypeScript 更了解某个值的详细信息。通常这会发生在你清楚地知道一个实体具有比它现有类型更确切的类型。
通过类型断言这种方式可以告诉编译器,“相信我,我知道自己在干什么”。类型断言好比其他语言里的类型转换,但是不进行特殊的数据检查和解构。它没有运行时的影响,只是在编译阶段起作用。
类型断言有两种形式:
3.1 “尖括号” 语法
let someValue: any = "this is a string";
let strLength: number = (someValue).length;
3.2 as 语法
let someValue: any = "this is a string";
let strLength: number = (someValue as string).length;
四、类型守卫
A type guard is some expression that performs a runtime check that guarantees the type in some scope. —— TypeScript 官方文档
类型保护是可执行运行时检查的一种表达式,用于确保该类型在一定的范围内。换句话说,类型保护可以保证一个字符串是一个字符串,尽管它的值也可以是一个数值。类型保护与特性检测并不是完全不同,其主要思想是尝试检测属性、方法或原型,以确定如何处理值。目前主要有四种的方式来实现类型保护
4.1 in 关键字
interface Admin {
name: string;
privileges: string[];
}
interface Employee {
name: string;
startDate: Date;
}
type UnknownEmployee = Employee | Admin;
function printEmployeeInformation(emp: UnknownEmployee) {
console.log("Name: " + emp.name);
if ("privileges" in emp) {
console.log("Privileges: " + emp.privileges);
}
if ("startDate" in emp) {
console.log("Start Date: " + emp.startDate);
}
}
4.2 typeof 关键字
function padLeft(value: string, padding: string | number) {
if (typeof padding === "number") {
return Array(padding + 1).join(" ") + value;
}
if (typeof padding === "string") {
return padding + value;
}
throw new Error(`Expected string or number, got '${padding}'.`);
}
typeof 类型保护只支持两种形式:typeof v === “typename” 和 typeof v !== typename,“typename” 必须是 “number”, “string”, “boolean” 或 “symbol”。但是 TypeScript 并不会阻止你与其它字符串比较,语言不会把那些表达式识别为类型保护。
4.3 instanceof 关键字
interface Padder {
getPaddingString(): string;
}
class SpaceRepeatingPadder implements Padder {
constructor(private numSpaces: number) {}
getPaddingString() {
return Array(this.numSpaces + 1).join(" ");
}
}
class StringPadder implements Padder {
constructor(private value: string) {}
getPaddingString() {
return this.value;
}
}
let padder: Padder = new SpaceRepeatingPadder(6);
if (padder instanceof SpaceRepeatingPadder) {
// padder的类型收窄为 'SpaceRepeatingPadder'
}
4.4 自定义类型保护的类型谓词
function isNumber(x: any): x is number {
return typeof x === "number";
}
function isString(x: any): x is string {
return typeof x === "string";
}
五、联合类型和类型别名
5.1 联合类型
联合类型通常与 null 或 undefined 一起使用
const sayHello = (name: string | undefined) => {
/* ... */
};
例如,这里 name 的类型是 string | undefined 意味着可以将 string 或 undefined 的值传递给sayHello 函数。
sayHello("Semlinker");
sayHello(undefined);
通过这个示例,你可以凭直觉知道类型 A 和类型 B 联合后的类型是同时接受 A 和 B 值的类型。
5.2 可辨识联合
TypeScript 可辨识联合(Discriminated Unions)类型,也称为代数数据类型或标签联合类型。它包含 3 个要点:可辨识、联合类型和类型守卫。
这种类型的本质是结合联合类型和字面量类型的一种类型保护方法。如果一个类型是多个类型的联合类型,且多个类型含有一个公共属性,那么就可以利用这个公共属性,来创建不同的类型保护区块
1.可辨识
可辨识要求联合类型中的每个元素都含有一个单例类型属性,比如:
enum CarTransmission {
Automatic = 200,
Manual = 300
}
interface Motorcycle {
vType: "motorcycle"; // discriminant
make: number; // year
}
interface Car {
vType: "car"; // discriminant
transmission: CarTransmission
}
interface Truck {
vType: "truck"; // discriminant
capacity: number; // in tons
}
在上述代码中,我们分别定义了 Motorcycle、 Car 和 Truck 三个接口,在这些接口中都包含一个 vType 属性,该属性被称为可辨识的属性,而其它的属性只跟特性的接口相关。
2.联合类型
基于前面定义了三个接口,我们可以创建一个 Vehicle 联合类型:
type Vehicle = Motorcycle | Car | Truck;
现在我们就可以开始使用 Vehicle 联合类型,对于 Vehicle 类型的变量,它可以表示不同类型的车辆
3.类型守卫
下面我们来定义一个 evaluatePrice 方法,该方法用于根据车辆的类型、容量和评估因子来计算价格,具体实现如下:
const EVALUATION_FACTOR = Math.PI;
function evaluatePrice(vehicle: Vehicle) {
return vehicle.capacity * EVALUATION_FACTOR;
}
const myTruck: Truck = { vType: "truck", capacity: 9.5 };
evaluatePrice(myTruck);
对于以上代码,TypeScript 编译器将会提示以下错误信息:
Property 'capacity' does not exist on type 'Vehicle'.
Property 'capacity' does not exist on type 'Motorcycle'.
原因是在 Motorcycle 接口中,并不存在 capacity 属性,而对于 Car 接口来说,它也不存在 capacity 属性。那么,现在我们应该如何解决以上问题呢?这时,我们可以使用类型守卫。下面我们来重构一下前面定义的 evaluatePrice 方法,重构后的代码如下:
function evaluatePrice(vehicle: Vehicle) {
switch(vehicle.vType) {
case "car":
return vehicle.transmission * EVALUATION_FACTOR;
case "truck":
return vehicle.capacity * EVALUATION_FACTOR;
case "motorcycle":
return vehicle.make * EVALUATION_FACTOR;
}
}
在以上代码中,我们使用 switch 和 case 运算符来实现类型守卫,从而确保在 evaluatePrice 方法中,我们可以安全地访问 vehicle 对象中的所包含的属性,来正确的计算该车辆类型所对应的价格。
5.3 类型别名
type Message = string | string[];
let greet = (message: Message) => {
// ...
};
六、交叉类型
TypeScript 交叉类型是将多个类型合并为一个类型。这让我们可以把现有的多种类型叠加到一起成为一种类型,它包含了所需的所有类型的特性。
interface IPerson {
id: string;
age: number;
}
interface IWorker {
companyId: string;
}
type IStaff = IPerson & IWorker;
const staff: IStaff = {
id: 'E1006',
age: 33,
companyId: 'EFT'
};
console.dir(staff)
在上面示例中,我们首先为 IPerson 和 IWorker 类型定义了不同的成员,然后通过 & 运算符定义了 IStaff 交叉类型,所以该类型同时拥有 IPerson 和 IWorker 这两种类型的成员。
七、TypeScript 函数
7.1 TypeScript 函数与 JavaScript 函数的区别
TypeScript | JavaScript |
---|---|
含有类型 | 无类型 |
箭头函数 | 箭头函数(ES2015) |
函数类型 | 无函数类型 |
必填和可选参数 | 所有参数都是可选的 |
默认参数 | 默认参数 |
剩余参数 | 剩余参数 |
函数重载 | 无函数重载 |
7.2 箭头函数
1.常见语法
myBooks.forEach(() => console.log('reading'));
myBooks.forEach(title => console.log(title));
myBooks.forEach((title, idx, arr) =>
console.log(idx + '-' + title);
);
myBooks.forEach((title, idx, arr) => {
console.log(idx + '-' + title);
});
2.使用示例
// 未使用箭头函数
function Book() {
let self = this;
self.publishDate = 2016;
setInterval(function () {
console.log(self.publishDate);
}, 1000);
}
// 使用箭头函数
function Book() {
this.publishDate = 2016;
setInterval(() => {
console.log(this.publishDate);
}, 1000);
}
7.3 参数类型和返回类型
function createUserId(name: string, id: number): string {
return name + id;
}
7.4 函数类型
let IdGenerator: (chars: string, nums: number) => string;
function createUserId(name: string, id: number): string {
return name + id;
}
IdGenerator = createUserId;
7.5 可选参数及默认参数
// 可选参数
function createUserId(name: string, age?: number, id: number): string {
return name + id;
}
// 默认参数
function createUserId(
name: string = "Semlinker",
age?: number,
id: number
): string {
return name + id;
}
7.6 剩余参数
function push(array, ...items) {
items.forEach(function (item) {
array.push(item);
});
}
let a = [];
push(a, 1, 2, 3);
7.7 函数重载
函数重载或方法重载是使用相同名称和不同参数数量或类型创建多个方法的一种能力。要解决前面遇到的问题,方法就是为同一个函数提供多个函数类型定义来进行函数重载,编译器会根据这个列表去处理函数的调用。
function add(a: number, b: number): number;
function add(a: string, b: string): string;
function add(a: string, b: number): string;
function add(a: number, b: string): string;
function add(a: Combinable, b: Combinable) {
if (typeof a === "string" || typeof b === "string") {
return a.toString() + b.toString();
}
return a + b;
}
在以上代码中,我们为 add 函数提供了多个函数类型定义,从而实现函数的重载。之后,可恶的错误消息又消失了,因为这时 result 变量的类型是 string 类型。在 TypeScript 中除了可以重载普通函数之外,我们还可以重载类中的成员方法。
方法重载是指在同一个类中方法同名,参数不同(参数类型不同、参数个数不同或参数个数相同时参数的先后顺序不同),调用时根据实参的形式,选择与它匹配的方法执行操作的一种技术。所以类中成员方法满足重载的条件是:在同一个类中,方法名相同且参数列表不同。下面我们来举一个成员方法重载的例子:
class Calculator {
add(a: number, b: number): number;
add(a: string, b: string): string;
add(a: string, b: number): string;
add(a: number, b: string): string;
add(a: Combinable, b: Combinable) {
if (typeof a === "string" || typeof b === "string") {
return a.toString() + b.toString();
}
return a + b;
}
}
const calculator = new Calculator();
const result = calculator.add("Semlinker", " Kakuqo");
这里需要注意的是,当 TypeScript 编译器处理函数重载时,它会查找重载列表,尝试使用第一个重载定义。如果匹配的话就使用这个。因此,在定义重载的时候,一定要把最精确的定义放在最前面。另外在 Calculator 类中,add(a: Combinable, b: Combinable){ } 并不是重载列表的一部分,因此对于 add 成员方法来说,我们只定义了四个重载方法
八、TypeScript 数组
8.1 数组解构
let x: number; let y: number ;let z: number;
let five_array = [0,1,2,3,4];
[x,y,z] = five_array;
8.2 数组展开运算符
let two_array = [0, 1];
let five_array = [...two_array, 2, 3, 4];
8.3 数组遍历
let colors: string[] = ["red", "green", "blue"];
for (let i of colors) {
console.log(i);
}
九、TypeScript 对象
9.1 对象解构
let person = {
name: "Semlinker",
gender: "Male",
};
let { name, gender } = person;
9.2 对象展开运算符
let person = {
name: "Semlinker",
gender: "Male",
address: "Xiamen",
};
// 组装对象
let personWithAge = { ...person, age: 33 };
// 获取除了某些项外的其它项
let { name, ...rest } = person;
十、TypeScript 接口
在面向对象语言中,接口是一个很重要的概念,它是对行为的抽象,而具体如何行动需要由类去实现。
TypeScript 中的接口是一个非常灵活的概念,除了可用于对类的一部分行为进行抽象以外,也常用于对「对象的形状(Shape)」进行描述。
10.1 对象的形状
interface Person {
name: string;
age: number;
}
let Semlinker: Person = {
name: "Semlinker",
age: 33,
};
10.2 可选 | 只读属性
interface Person {
readonly name: string;
age?: number;
}
只读属性用于限制只能在对象刚刚创建的时候修改其值。此外 TypeScript 还提供了 ReadonlyArray 类型,它与 Array 相似,只是把所有可变方法去掉了,因此可以确保数组创建后再也不能被修改。
let a: number[] = [1, 2, 3, 4];
let ro: ReadonlyArray = a;
ro[0] = 12; // error!
ro.push(5); // error!
ro.length = 100; // error!
a = ro; // error!
十一、TypeScript 类
11.1 类的属性与方法
在面向对象语言中,类是一种面向对象计算机编程语言的构造,是创建对象的蓝图,描述了所创建的对象共同的属性和方法。
在 TypeScript 中,我们可以通过 Class 关键字来定义一个类:
class Greeter {
// 静态属性
static cname: string = "Greeter";
// 成员属性
greeting: string;
// 构造函数 - 执行初始化操作
constructor(message: string) {
this.greeting = message;
}
// 静态方法
static getClassName() {
return "Class name is Greeter";
}
// 成员方法
greet() {
return "Hello, " + this.greeting;
}
}
let greeter = new Greeter("world");
那么成员属性与静态属性,成员方法与静态方法有什么区别呢?这里无需过多解释,我们直接看一下以下编译生成的 ES5 代码:
"use strict";
var Greeter = /** @class */ (function () {
// 构造函数 - 执行初始化操作
function Greeter(message) {
this.greeting = message;
}
// 静态方法
Greeter.getClassName = function () {
return "Class name is Greeter";
};
// 成员方法
Greeter.prototype.greet = function () {
return "Hello, " + this.greeting;
};
// 静态属性
Greeter.cname = "Greeter";
return Greeter;
}());
var greeter = new Greeter("world");
11.2 访问器
在 TypeScript 中,我们可以通过 getter 和 setter 方法来实现数据的封装和有效性校验,防止出现异常数据。
let passcode = "Hello TypeScript";
class Employee {
private _fullName: string;
get fullName(): string {
return this._fullName;
}
set fullName(newName: string) {
if (passcode && passcode == "Hello TypeScript") {
this._fullName = newName;
} else {
console.log("Error: Unauthorized update of employee!");
}
}
}
let employee = new Employee();
employee.fullName = "Semlinker";
if (employee.fullName) {
console.log(employee.fullName);
}
还有好多,未完,待续。。。