这是我参与8月更文挑战的第26天,活动详情查看:8月更文挑战
往期文章
上一篇文章介绍了 typescript 的基础类型和一些其他的内容,本文就来记录下 ts 的一些常用功能。
类型断言
类型断言,简而言之,就是对某个变量的类型进行说明。
比如var times = 2;这个语句生命的times变量是一个Number类型。
当然有人说我知道啊,代码我自己写的我能不知道吗?其实断言诗给 ts 看的,ts 用断言来帮你判断你的代码逻辑中是否出现了问题,而且别人也能通过代码中的类型断言很快理解这些变量的意思。
总之,类型断言是 ts 非常牛逼的功能,简直是小母牛坐火箭--牛逼上天了~
那么在 typescript,如何使用类型断言呢?
let sth: any = 'this are sth special here'
由于sth是个任意类型,所以它可能存在或者不存在length属性,假如我们想读取length属性,就需要断言它是一个字符串类型。这里有两种方法,其中在 tsx 中必须使用第二种
let sthLength: number = (<string>sth).lengthlet sthLength2: number = (sth as string).length
类型推断
针对每一个变量写类型断言,是非常浪费时间的事情,ts 显然也明白这个道理,因此,ts 提供了类型推断这样的功能。
所谓的类型推断,就是我们写代码定义变量的时候不必须明确的指明类型,后续 ts 可以根据变量的值自动推倒该变量的类型,所以类型推断要求定义变量的时候必须进行赋值
let myFavoriteNumber = 'seven';
myFavoriteNumber = 7; //error
如果定义的时候没有赋值,不管之后有没有赋值,都会被推断成 any 类型而完全不被类型检查
let myFavoriteNumber;
myFavoriteNumber = 'seven'; //OK
myFavoriteNumber = 7; //ok
联合类型
很多时候,一个变量的类型不一定是固定的,比如我们经常对一个间隔执行进行初始化赋值:
var timer = null
timer = setTimeout(console.log, 1000)
针对这种情况,ts 推出了联合类型的概念,用于表示变量的取值范围可以为多种类型中的一种或者几种的集合。
let myFavoriteNumber: string | number;
当 TypeScript 不确定一个联合类型的变量到底是哪个类型的时候,我们只能访问此联合类型的所有类型里共有的属性或方法
function getLength(something: string | number): number {
something.length; //error
something.toString(); //ok
}
另外,已经定义为联合类型的变量在被赋值的时候,ts 会根据类型推论的规则推断出一个确定的类型
let myFavoriteNumber: string | number;
myFavoriteNumber = 'seven';
console.log(myFavoriteNumber.length); // 5
myFavoriteNumber = 7;
console.log(myFavoriteNumber.length); //error
接口
接口(interface) 本质上是对一个事物的描述,在 js 中本身没有面向对象编程的概念,所以通过对对象或者函数进行描述,从而在使用该对象或者函数的层面上形成了约定。
为了和对象函数等普通变量区分,接口一般首字母大写,并且赋值的时候,变量的形状必须和接口的形状保持一致。
所以,接口可以用于三个方面:
- 用于对类的一部分行为进行抽象,
- 也常用于对「对象的形状(Shape)」进行描述,如函数,
- 也可用于二者的混合。
interface Person {
name: string;
age: number;
}
interface Sum{
(x:number,y:number):number
}
let sum:Sum = (a,b)=> { return a+b }
任意属性与可选属性
但是通常说来,一个已经定义好的对象在使用上和定义总是不尽相同,这种情况下,接口定义可以使用可选的属性与任意的属性。
可选的属性用?来表示,任意的属性用[]来表示
interface Person {
age?: string; //可选的属性
}
interface Animal {
[propName: string]: string; //任意的属性
}
需要注意:如果定义了任意属性,那么确定属性和可选属性的类型都必须是它的类型的子集
interface Person {
[propName: string]: string; //任意的属性的值类型都为 string
age?: number; //error 因为定义了任意属性,且任意属性为 string 类型,所以这里 number 类型是不对的
}
只读属性
有时候我们希望定义的对象中某些属性不可被外界更改,言外之意这个属性是只读的,因此这里需要用到只读属性
只读属性:readonly 可以用来定义某个属性只读,而不能被再次赋值
interface Person {
readonly id: number;
}
let tom: Person = {
id: 89757
}
tom.id = 1 //error
接口继承
面相对象的三个基本特征是:继承,封装和多态。
为了更好的支持 js 面向对象编程,ts 规定接口是可以继承的。继承用extends来表示。
加入我们有一个基础类,叫做形状,那么这个形状有个color属性
interface Shape {
color: string
}
然后我们需要实现一个四边形的接口,就可以继承形状这个基础类,从而获得color这个属性
interface Square extends Shape {
sideLength: number
}
let square = {} as Square
square.color = 'blue'
square.sideLength = 10
继承是可以一对多继承的,也就是说,一个接口可以继承自多个接口
interface PenStroke extends Shape, Square{
width: number
}
类数组
类数组和普通的数组不一样,类数组是只具有索引和长度属性的对象,比如document.getElementsByClass的返回值
因此类数组不应该用普通数组的方式来定义,而应该用接口。
ts 中针对类数组都已经定义好了接口,因此我们可以直接使用,如 IArguments, NodeList, HTMLCollection, HTMLElement 等
function sum() {
let args: IArguments = arguments;
}
var p:HTMLElement = document.createElement('p')
内置对象
所谓内置对象类型,出了几个基础和复杂类型,还有Date、Error等。
在 TypeScript 中将变量定义为内置对象类型,比如我们定义一个错误
let e: Error = new Error('Error occurred');
或者定义一个日期类型
let d: Date = new Date();
或者正则表达式
let r: RegExp = /[a-z]/;
同时,js 还有两个非常重要的内置对象,那就是 DOM 和 BOM,相信所有学过 javascrit 的同学,都对这两个对象印象深刻吧。
DOM 和 BOM 提供的内置对象有:Document、HTMLElement、Event、NodeList 等等~
let body: HTMLElement = document.body;
let allDiv: NodeList = document.querySelectorAll('div');
document.addEventListener('click', function(e: MouseEvent) {
// Do something
})
函数
函数和其他变量最大的区别在于:一个函数有输入和输出。
要在 TypeScript 中对其进行约束,需要把输入和输出都考虑到。
ts 提供的函数的约束也是分成了两种,一种是函数声明,另一种是函数表达式。
其中,函数声明的写法如下:
function sum(x: number, y: number): number {
return x + y;
}
函数表达式的写法与上面略有区别,因为表达式还需要对声明的函数变量进行描述
let mySum: (x: number, y: number) => number = function (x: number, y: number): number {
return x + y;
};
上面代码中(x: number, y: number) => number这一部分就是对mySum的描述
注意看,上面函数表达式中的=>与 ES6 中的箭头函数是不同的,这里的=> 的左边表示函数的输入的约束,右边表示函数的输出的约束
当然,这样的写法通常对阅读的人造成困扰,所以,ts 提供了另外一种描述方式
使用接口对函数进行约束
我们也可以用接口对函数进行约束,我们在前面也写过这个例子,这样看起来可能比表达式更好理解
interface MySum {
(x: number, y: number): number;
}
let sum: MySum = function(x: number, y: number) {
return x + y
}
可选参数
有些时候,尤其是在一些插件对方法的封装中,有些参数并非是必传参数,如果用户没有传递这样的参数,我们可以在内部对其做默认的赋值
所以 ts 提供了可选参数。
可选参数:在函数中也使用 ? 来描述可选的参数,其中可选的参数要放在必需参数的后面
function buildName(firstName: string, lastName?: string) {
if (lastName) {
return firstName + ' ' + lastName;
} else {
return firstName + 'Cat';
}
}
let tomcat = buildName('Tom', 'Cat');
let tom = buildName('Tom');
参数默认值
ES6 增加了函数参数默认值的用法(a=1)=>a++
对于这种情况,ts 使用参数默认值来约束。
参数默认值:TypeScript 会将添加了默认值的参数识别为可选参数,这时就不受「可选参数必须接在必需参数后面」的限制了
function buildName(firstName: string = 'Tom', lastName: string) {
return firstName + ' ' + lastName;
}
let tomcat = buildName('Tom', 'Cat');
let cat = buildName(undefined, 'Cat');
参数解构赋值
同时,ts 针对 ES6 的解构也提供了约束写法
function push(array: any[], ...items: any[]) {
items.forEach(function(item) {
array.push(item);
});
}
let a = [];
push(a, 1, 2, 3);
函数重载
函数重载:函数重载是函数的一种特殊情况,为方便使用,C++允许在同一范围中声明几个功能类似的同名函数,但是这些同名函数的形式参数(指参数的个数、类型或者顺序)必须不同,也就是说用同一个函数完成不同的功能。
然而 javascript 中函数不存在重载的概念,后声明的函数变量会覆盖之前的同名变量,所以我们通常情况下通过对参数的判断来实现伪重载
function reverse(x: number): number;
function reverse(x: string): string;
function reverse(x: number | string): number | string {
if (typeof x === 'number') {
return Number(x.toString().split('').reverse().join(''));
} else if (typeof x === 'string') {
return x.split('').reverse().join('');
}
}
上面的代码中,我们重复定义了多次函数reverse,前几次都是函数定义,最后一次是函数实现。
TypeScript 会优先从最前面的函数定义开始匹配,所以多个函数定义如果有包含关系,需要优先把精确的定义写在前面
接口与函数的混合
我们在前面接口那个小结已经说了接口可以用于类与函数的混合。
这是因为类中既可以定义属性,也可以定义属于类的方法。
interface Counter {
(start: number): string
interval: number
reset(): void
}
function getCounter(): Counter {
let counter = (function (start: number) { }) as Counter
counter.interval = 123
counter.reset = function () { }
return counter
}
let c = getCounter()
c(10)
c.reset()
c.interval = 5
类
ES6 提供了类的概念,从而更方便的使用面向对象的编程思想。
类可以看多是程序中最基础的单元,比如坦克大战中,坦克可以看作类,子弹可以看作类,墙也可以看作类。
所以类本身可以定义多个属性和方法。针对每个属性和方法,出了本身的类型约束外,还有一些其他的约束,比如属性是公有的还是私有的。
修饰符
类的修饰符有很多种:
| 修饰符 | 含义 | 备注 |
|---|---|---|
| public | 公共属性或方法 | 可以任意访问 |
| private | 私有属性或方法 | 不可被外部访问,不可继承 |
| protected | 被保护的属性或方法 | 只能被继承 |
| static | 静态的属性或方法 | 只存在于类本身 |
| readonly | 只读的属性或方法 | 必须在声明时或构造函数里被初始化 |
| abstract | 抽象类或方法 | 抽象类不能实例话,抽象方法必须被子类实现 |
public 修饰的属性或方法是公有的,可以在任何地方被访问到,也可以被继承,默认所有的属性和方法都是 public 的
class Animal {
public name: string;
public constructor(name: string) {
this.name = name;
}
}
let a = new Animal('Jack');
console.log(a.name); // Jack
a.name = 'Tom';
console.log(a.name); // Tom
private 修饰的属性或方法是私有的,不能在声明它的类的外部访问,也不能被继承
class Animal {
private name: string;
public constructor(name: string) {
this.name = name;
}
}
let a = new Animal('Jack');
console.log(a.name); // error
a.name = 'Tom'; //error
class Cat extends Animal {
constructor(name) {
super(name);
console.log(this.name); //error
}
}
当构造函数的修饰为 private 时,该类不允许被继承或者实例化
class Animal {
public name: string;
private constructor (name: string) {
this.name = name;
}
}
class Cat extends Animal { //error
constructor (name) {
super(name);
}
}
let a = new Animal('Jack');//error
当构造函数修饰为 protected 时,该类只允许被继承
class Animal {
public name: string;
protected constructor (name: string) {
this.name = name;
}
}
class Cat extends Animal {
constructor (name) {
super(name);
}
}
let a = new Animal('Jack'); //error
protected 修饰的属性或方法是受保护的,它和 private 类似,区别是它在子类中也是允许被访问的
class Animal {
protected name: string;
public constructor(name: string) {
this.name = name;
}
}
class Cat extends Animal {
constructor(name) {
super(name);
console.log(this.name); //ok
}
}
static 是ES7中的一个提案,用来定义静态的属性,这些属性存在于类本身上面而不是类的实例上
class Animal {
static num: number = 42;
constructor() {
// ...
}
}
console.log(Animal.num); // 42
readonly 只读属性,必须在声明时或构造函数里被初始化
class Person {
readonly age: number
constructor(public readonly name: string, age: number) {
this.name = name
this.age = age
}
}
let a = new Animal('Jack',25);
console.log(a.name); // Jack
a.name = 'Tom'; //error
a.age = 26; //error
abstract 用于定义抽象类和其中的抽象方法,首先,抽象类是不允许被实例化,其次,抽象类中的抽象方法必须被子类实现
abstract class Animal {
public name: string;
public constructor(name) {
this.name = name;
}
public abstract sayHi(): void;
}
let a = new Animal('Jack'); //error
class Cat extends Animal {
public sayHi(): void { //继承类必须实现这个方法
console.log(`Meow, My name is ${this.name}`);
}
public eat(): void {
console.log(`${this.name} is eating.`);
}
}
let cat = new Cat('Tom');
存取器:使用 getter 和 setter 可以改变属性的赋值和读取行为
// 先检查用户密码是否正确,然后再允许其修改员工信息
class Employee {
private _fullName: string
private _passcode: string
constructor(readonly passcode: string){
this._passcode = passcode
}
get fullName(): string {
return this._fullName
}
set fullName(newName: string) {
if (this._passcode && this._passcode == 'secret passcode') {
this._fullName = newName
}
else {
console.log('Error: Unauthorized update of employee!')
}
}
}
let employee = new Employee('secret passcode')
employee.fullName = 'Bob Smith'
if (employee.fullName) {
console.log(employee.fullName)
}
接口与类
凡是允许使用接口的地方也允许使用类
class Point {
x: number
y: number
}
interface Point3d extends Point {
z: number
}
let point3d: Point3d = {x: 1, y: 2, z: 3}
类实现接口
interface 不仅可以用来对 class 进行属性描述,而且也可以实现 class 的方法,这叫做 类实现接口。
假如不同类之间有一些共有的特性,就可以把特性提取成 interfaces,用 implements 关键字来实现。
例如:门是一个类,防盗门是门的子类。如果防盗门有一个报警器的功能,我们可以简单的给防盗门添加一个报警方法。这时候如果有另一个类,车,也有报警器的功能,就可以考虑把报警器提取出来,作为一个接口,防盗门和车都去实现它
interface Alarm {
alert();
}
class Door {
}
class SecurityDoor extends Door implements Alarm {
alert() {
console.log('SecurityDoor alert');
}
}
class Car implements Alarm {
alert() {
console.log('Car alert');
}
}
同一个类也可以实现多个接口,从而实现在类中添加多个约束。
interface Alarm {
alert();
}
interface Light {
lightOn();
lightOff();
}
class Car implements Alarm, Light {
alert() {
console.log('Car alert');
}
lightOn() {
console.log('Car light on');
}
lightOff() {
console.log('Car light off');
}
}
接口继承类
前面已经说过,接口是可以互相继承的,其实接口也可以继承类。
class Point {
x: number;
y: number;
}
interface Point3d extends Point {
z: number;
}
let point3d: Point3d = {x: 1, y: 2, z: 3};