第8天typescript的学习

448 阅读25分钟

ts中文学习网站:www.tslang.cn/docs/handbo…

1.初识ts

image.png

tcs --init生成配置文件

image.png

自动编译操作流程:

image.png

image.png

类型声明给变量设置了类型,使用变量只能存储某种类型的值

js的类型分为两种:原始数据类型和对象类型,原始数据类型包括:布尔值、数值、字符串、null、undefined以及ES6中的新类型Symbol和ES10中的新类型BigInt.

基础类型:

(()=>{
    function sayHi(str:String) {
        return "你好"+str;
    }
    sayHi("ljb");
    let a:Number = 30;
    console.log(a);
    //布尔类型
    let isDone:boolean = false;
    //数字
    let num:number = 10;//十进制
    let num1:number = 0b10101;//二进制
    let num2:number = 0o107;//八进制
    let num3:number = 0xaf;//十六进制
     //字符串
     let str1:string = '123';
     //undefined和null  用的不多
     let u:undefined = undefined;
     let n:null = null;
     //注意:undefined和null   还可以作为其他类型的子类型
     //注意:可以把undefined和null 赋值给其他类型的变量
     let b:number = undefined;
     let str2:string = null;
    //数组
    let list1:number[] = [1,2,3];
    let list2:Array<number> = [1,2,3];
    //元组

})();

数组:

(()=>{
    //数组
    let arr1:number[] = [1,2,3];
    let arr2:Array<number> = [1,2,3];
})();

对象: 也就是除numberstringbooleansymbolnullundefined之外的类型。

//object  表示非原始类型,也就是除number,string,boolean,symbol,null或undefined之外的类型。

let obj:object = {};
//字面量
//obj = 123;报错
//obj = '';报错
obj = null;
obj = undefined;
obj = [];
obj = new String();
obj = String;

any:

let h:any = 123;
h = true;
h = {};
h = [];
let newArr:any[] = [100.243,2,4,"",false];
console.log(newArr[0].split(''));

void:

//void 空值,表示,没有任何返回值的函数
function fun1():void{
    console.log(123);
}
console.log(fun1());

//没有任何类型
let v:void =undefined;

类型推断:

// 如果没有明确的指定类型,
// 那么 TypeScript 会依照类型推论的规则推断出一个类型。
// 2种情况
// 1.定义变量的时候,直接给变量赋值,则定义类型为对应的类型
// 2.定义变量的时候,没有赋值,则定义类型为any类型
let t = 123;
let g;
g=123;
g="";
g={};
g=[];

联合类型:

表示取值可以为多种类型中的一种。
// 表示取值可以为多种类型中的一种。
// 联合类型使用 | 分隔每个类型
// 当 TypeScript 不确定一个联合类型的变量到底是哪个类型的时候,
// 我们只能访问此联合类型的所有类型里共有的属性或方法:
// 联合类型的变量在被赋值的时候,会根据类型推论的规则推断出一个类型:
let f:boolean | number | string = true;
f = 123;//再次赋值,走类型推断,给变量定义一个类型
f = false;
f = "ljb";
console.log(f.split(""));

接口:它是对行为的抽象,用于对「对象的形状(Shape)」进行描述,理解为一种约束。接口一般首字母大写。定义的变量比接口少了一些属性是不允许的:可见,赋值的时候,变量的形状必须和接口的形状保持一致

interface Person {
    readonly id:number,//只读属性  有时候我们希望对象中的一些字段只能在创建的时候被赋值,那么可以用 readonly 定义只读属性:
    name:string,
    age:number,
    sex?:string,//?可选属性  可选属性的含义是该属性可以不存在。
    [propName:string]:string|number,//任意类型  有时候我们希望一个接口允许有任意的属性,
}
let tom:Person = {
    id:123456789,//注意,只读的约束存在于第一次给对象赋值的时候,而不是第一次给只读属性赋值的时候:
    name:'Tom',
    age:25,
    score:"99",//使用 [propName: string] 定义了任意属性取 string 类型的值。
    // 需要注意的是,一旦定义了任意属性,那么确定属性和可选属性的类型都必须是它的类型的子集:
    // 一个接口中只能定义一个任意属性。如果接口中有多个类型的属性,则可以在任意属性中使用联合类型:
}

数组类型:

interface INewArray {
    [index:number]:number,//任意属性,index表示数组中的下标
    length:number,
    callee:Function
}
// 用 any 表示数组中允许出现任意类型:
let list:any[] = [1,"2",{name:"ljb"}];
// 我们除了约束当索引的类型是数字时,值的类型必须是数字之外,也约束了它还有 length 和 callee 两个属性。
// 事实上常用的类数组都有自己的接口定义,如 IArguments, NodeList, HTMLCollection 等:

function sum() {
    let args:{
        [index:number]:number,
        length:number,
        callee:Function
    } = arguments;
}

function add() {
    let args:IArguments = arguments;
}

重点:函数 在 JavaScript 中,有两种常见的定义函数的方式——函数声明(Function Declaration)和函数表达式(Function Expression):

interface ISearchFunc {
    // (参数:类型,...):返回值类型
    (a:string,b:string):boolean;
}
const fun1:ISearchFunc = function(a:string,b:string):boolean {
    // return true;
    return a.search(b) !==-1;
};
console.log(fun1("123","1"));

一个函数有输入和输出,要在 TypeScript 中对其进行约束,需要把输入和输出都考虑到,其中函数声明的类型定义较简单:

function sum(x: number, y: number): number {
    return x + y;
}
注意,**输入多余的(或者少于要求的)参数,是不被允许的**:
//函数声明
function add(a:number,b:number) :number{
    return a+b;
}
//函数表达式
console.log(add(1,2));
let c:number = add(1,2);
console.log(c);
//函数表达式
let add2 = function(a:number,b:number):number {
    return a+b;
}

//函数完整的写法
let add3:(a:number,b:number)=>number = function(a:number,b:number):number {
    return a+b;
}
//注意不要混淆了 TypeScript 中的 `=>` 和 ES6 中的 `=>`。

//在 TypeScript 的类型定义中,`=>` 用来表示函数的定义,左边是输入类型,需要用括号括起来,右边是输出类型。
用 `?` 表示可选的参数:
let getName = function(x:string,z?:string,y:string='Cat'):string {
    return x+z+y;
}
//可选参数? 必选参数不能位于可选参数后
console.log(getName('zhangsan',"123"));
console.log(getName('zhangsan','san',"456"));
//默认参数可以放在必选参数以及可选参数之后的
console.log(getName('zhangsan'));
//需要注意的是,可选参数必须接在必需参数后面。换句话说,**可选参数后面不允许再出现必需参数了**
//在 ES6 中,我们允许给函数的参数添加默认值,**TypeScript 会将添加了默认值的参数识别为可选参数**:
//剩余参数
function fn(x:string,y:string,...args) {
    console.log(x,y,args);
}
fn("1","2",3,4,5,6,7,8,9);

//函数重载:函数名相同,形参不同的多个函数
//数字 相加;字符串  拼接;

//函数重载声明
// 这时,我们可以使用重载定义多个 reverse 的函数类型
function newAdd(x:string,y:string):string;
function newAdd(x:number,y:number):number;

function newAdd(x:string|number,y:string|number):string|number {
    if(typeof x == 'string' && typeof y =='string') {
        return x+y;//字符串拼接
    }else if(typeof x == 'number' && typeof y =='number') {
        return x+y;//数字相加
    }
}
//然而这样有一个缺点,就是不能够精确的表达,
// 输入为数字的时候,输出也应该为数字,输入为字符串的时候,输出也应该为字符串。
console.log(newAdd(1,2));
console.log(newAdd("张","三"));

类型断言:类型断言(Type Assertion)可以用来手动指定一个值的类型。

//定义一个函数,获取到一个数字或字符串的长度
// 将一个联合类型断言为其中一个类型
//当 TypeScript 不确定一个联合类型的变量到底是哪个类型的时候,
//我们**只能访问此联合类型的所有类型中共有的属性或方法**:
//而有时候,我们确实需要在还不确定类型的时候就访问其中一个类型特有的属性或方法
function getLength(x:string|number):number {
    if((x as string).length) {
        return (<string>x).length;
    }else {
        return x.toString().length;
    }
    
}
console.log(getLength("123"));
console.log(getLength(123));

需要注意的是,类型断言只能够「欺骗」TypeScript 编译器,无法避免运行时的错误,反而滥用类型断言可能会导致运行时错误:

// 将任何一个类型断言为 any
// 理想情况下,TypeScript 的类型系统运转良好,每个值的类型都具体而精确。
//any类型是允许访问任何属性和方法的
(window as any).foo = 10;
// 在 any 类型的变量上,访问任何属性都是允许的。
// 需要注意的是,将一个变量断言为 any 可以说是解决 TypeScript 中类型问题的最后一个手段。
// 它极有可能掩盖了真正的类型错误,所以如果不是非常确定,就不要使用 as any。
// 总之,一方面不能滥用 as any,另一方面也不要完全否定它的作用,
// 我们需要在类型的严格性和开发的便利性之间掌握平衡

// 将 any 断言为一个具体的类型
function abc(x:any,y:any):any {
    return x+y;
}
let a = abc(1,2) as number;
let b = abc("1","2") as string;

2.ts进阶

类型别名:类型别名用来给一个类型起个新名字。类型别名常用于联合类型。

type s = string;
let str:s = "123";

type all = string|number|boolean;
let x:all = 123;
x="";
let y:all = true;
y=123;

字符串字面量类型:字符串字面量类型用来约束取值只能是某几个字符串中的一个

image.png

// 字符串字面量类型用来约束取值只能是某几个字符串中的一个
type stringType = "张三丰" | "张大炮" | "张无忌";
let names:stringType = "张无忌";

元组:数组合并了相同类型的对象,而元组(Tuple)合并了不同类型的对象。

let arr:number[] = [1,2,3,4,5,6];

let Tarr:[string,number] = ["123",465];
//添加内容的时候,需要的是string | number 类型
Tarr.push(798);
Tarr.push("014");
// Tarr.push(true);//报错

当添加越界的元素时,它的类型会被限制为元组中每个类型的联合类型.

枚举:枚举(Enum)类型用于取值被限定在一定范围内的场景,比如一周只能有七天,颜色限定为红绿蓝等。

使用枚举类型给一组数据赋予名称;可以通过名称取值,也可以通过值去取名称;

enum Days {Sun, Mon, Tue, Wed, Thu, Fri, Sat};

枚举成员会被赋值为从 `0` 开始递增的数字,同时也会对枚举值到枚举名进行反向映射:
console.log(Days["Sun"] === 0); // true
console.log(Days["Mon"] === 1); // true
console.log(Days["Tue"] === 2); // true
console.log(Days["Sat"] === 6); // true

console.log(Days[0] === "Sun"); // true
console.log(Days[1] === "Mon"); // true
console.log(Days[2] === "Tue"); // true
console.log(Days[6] === "Sat"); // true

我们也可以给枚举项手动赋值:
enum Days {Sun = 7, Mon = 1, Tue, Wed, Thu, Fri, Sat};

console.log(Days["Sun"] === 7); // true
console.log(Days["Mon"] === 1); // true
console.log(Days["Tue"] === 2); // true
console.log(Days["Sat"] === 6); // true
未手动赋值的枚举项会接着上一个枚举项递增。
如果未手动赋值的枚举项与手动赋值的重复了,TypeScript 是不会察觉到这一点的:
手动赋值的枚举项可以不是数字,此时需要使用类型断言来让 tsc 无视类型检查 (编译出的 js 仍然是可用的):
enum Days {Sun = 7, Mon, Tue, Wed, Thu, Fri, Sat = <any>"S"};
var Days;
(function (Days) {
    Days[Days["Sun"] = 7] = "Sun";
    Days[Days["Mon"] = 8] = "Mon";
    Days[Days["Tue"] = 9] = "Tue";
    Days[Days["Wed"] = 10] = "Wed";
    Days[Days["Thu"] = 11] = "Thu";
    Days[Days["Fri"] = 12] = "Fri";
    Days[Days["Sat"] = "S"] = "Sat";
})(Days || (Days = {}));

当然,手动赋值的枚举项也可以为小数或负数,此时后续未手动赋值的项的递增步长仍为 1

手动赋值,尽量不要写一些重复的值

enum Days {Sun = 7, Mon = 1.5, Tue, Wed, Thu, Fri, Sat};

console.log(Days["Sun"] === 7); // true
console.log(Days["Mon"] === 1.5); // true
console.log(Days["Tue"] === 2.5); // true
console.log(Days["Sat"] === 6.5); // true

常数项和计算所得项§

枚举项有两种类型:常数项(constant member)和计算所得项(computed member)。

前面我们所举的例子都是常数项,一个典型的计算所得项的例子:

enum Color {Red, Green, Blue = "blue".length};

上面的例子中,"blue".length 就是一个计算所得项。

上面的例子不会报错,但是如果紧接在计算所得项后面的是未手动赋值的项,那么它就会因为无法获得初始值而报错

enum Color {Red = "red".length, Green, Blue};

// index.ts(1,33): error TS1061: Enum member must have initializer.
// index.ts(1,40): error TS1061: Enum member must have initializer.

下面是常数项和计算所得项的完整定义,部分引用自中文手册 - 枚举

当满足以下条件时,枚举成员被当作是常数:

  • 不具有初始化函数并且之前的枚举成员是常数。在这种情况下,当前枚举成员的值为上一个枚举成员的值加 1。但第一个枚举元素是个例外。如果它没有初始化方法,那么它的初始值为 0

  • 枚举成员使用常数枚举表达式初始化。常数枚举表达式是 TypeScript 表达式的子集,它可以在编译阶段求值。当一个表达式满足下面条件之一时,它就是一个常数枚举表达式:

    • 数字字面量
    • 引用之前定义的常数枚举成员(可以是在不同的枚举类型中定义的)如果这个成员是在同一个枚举类型中定义的,可以使用非限定名来引用
    • 带括号的常数枚举表达式
    • +-~ 一元运算符应用于常数枚举表达式
    • +-*/%<<>>>>>&|^ 二元运算符,常数枚举表达式做为其一个操作对象。若常数枚举表达式求值后为 NaN 或 Infinity,则会在编译阶段报错

计算所得项需要放置在已经确定赋值的枚举项之前,后面不能存放未手动赋值的枚举项;

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

常数枚举

常数枚举是使用 const enum 定义的枚举类型:

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 */];

假如包含了计算成员,则会在编译阶段报错:

const enum Color {Red, Green, Blue = "blue".length};

// index.ts(1,38): error TS2474: In 'const' enum declarations member initializer must be constant expression.

外部枚举§

外部枚举(Ambient Enums)是使用 declare enum 定义的枚举类型:

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

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

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

上例的编译结果是:

var directions = [Directions.Up, Directions.Down, Directions.Left, Directions.Right];

外部枚举与声明语句一样,常出现在声明文件中。

同时使用 declare 和 const 也是可以的:

declare 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 */];

传统方法中,JavaScript 通过构造函数实现类的概念,通过原型链实现继承。而在 ES6 中,我们终于迎来了 class

类:描述了所创建的对象共同的属性和方法。

TypeScript 除了实现了所有 ES6 中的类的功能以外,还添加了一些新的用法。

class Animal {
    name:String;
    age:Number;
    constructor(name:string,age:number) {
        this.name = name;
        this.age = age;
    }
    sayHi(str:string) {
        console.log('hi '+str);
    }
}
class Dog extends Animal {
    constructor(name:string,age:number) {
        super(name,age);
    }
    sayHi(): void {
        console.log("子类中的方法");
        super.sayHi("调用父类的方法");
    }
}

const a = new Animal('小猫',3);
a.sayHi("kakaka");
const d = new Dog('小够',5);
d.sayHi();

存取器§

使用 getter 和 setter 可以改变属性的赋值和读取行为:

class Animal {
  constructor(name) {
    this.name = name;
  }
  get name() {
    return 'Jack';
  }
  set name(value) {
    console.log('setter: ' + value);
  }
}

let a = new Animal('Kitty'); // setter: Kitty
a.name = 'Tom'; // setter: Tom
console.log(a.name); // Jack
//存取器,可以帮助我们控制对象成员的访问
class Name {
    firstName:String;
    lastName:String;
    constructor(firstName:String,lastName:String) {
        this.firstName = firstName;
        this.lastName = lastName;
    }
    get fullName() {
        return this.firstName+"-"+this.lastName;
    }
    set fullName(value) {
        let names = value.split("-");
        this.firstName = names[0];
        this.lastName = names[1];
    }
}

const n = new Name("李","静波");
console.log(n);
console.log(n.fullName);
n.fullName = "张-三丰";
console.log(n.firstName,n.lastName);

静态方法§

使用 static 修饰符修饰的方法称为静态方法,它们不需要实例化,而是直接通过类来调用:

class Animal {
  static isAnimal(a) {
    return a instanceof Animal;
  }
}

let a = new Animal('Jack');
Animal.isAnimal(a); // true
a.isAnimal(a); // TypeError: a.isAnimal is not a function

TypeScript 中类的用法§

public private 和 protected§

TypeScript 可以使用三种访问修饰符(Access Modifiers),分别是 publicprivate 和 protected

  • public 修饰的属性或方法是公有的,可以在任何地方被访问到,默认所有的属性和方法都是 public 的
  • private 修饰的属性或方法是私有的,不能在声明它的类的外部访问
  • protected 修饰的属性或方法是受保护的,它和 private 类似,区别是它在子类中也是允许被访问的

下面举一些例子:

class Animal {
  public name;
  public constructor(name) {
    this.name = name;
  }
}

let a = new Animal('Jack');
console.log(a.name); // Jack
a.name = 'Tom';
console.log(a.name); // Tom

上面的例子中,name 被设置为了 public,所以直接访问实例的 name 属性是允许的。

很多时候,我们希望有的属性是无法直接存取的,这时候就可以用 private 了:

class Animal {
  private name;
  public constructor(name) {
    this.name = name;
  }
}

let a = new Animal('Jack');
console.log(a.name);
a.name = 'Tom';

// index.ts(9,13): error TS2341: Property 'name' is private and only accessible within class 'Animal'.
// index.ts(10,1): error TS2341: Property 'name' is private and only accessible within class 'Animal'.

需要注意的是,TypeScript 编译之后的代码中,并没有限制 private 属性在外部的可访问性。private不能在声明它的类的外部访问,包括其子类,但是这个属性和方法是可以被继承的。

上面的例子编译后的代码是:

var Animal = (function () {
  function Animal(name) {
    this.name = name;
  }
  return Animal;
})();
var a = new Animal('Jack');
console.log(a.name);
a.name = 'Tom';

使用 private 修饰的属性或方法,在子类中也是不允许访问的:

class Animal {
  private name;
  public constructor(name) {
    this.name = name;
  }
}

class Cat extends Animal {
  constructor(name) {
    super(name);
    console.log(this.name);
  }
}

// index.ts(11,17): error TS2341: Property 'name' is private and only accessible within class 'Animal'.

而如果是用 protected 修饰,则允许在子类中访问:

class Animal {
  protected name;
  public constructor(name) {
    this.name = name;
  }
}

class Cat extends Animal {
  constructor(name) {
    super(name);
    console.log(this.name);
  }
}

当构造函数修饰为 private 时,该类不允许被继承或者实例化:

class Animal {
  public name;
  private constructor(name) {
    this.name = name;
  }
}
class Cat extends Animal {
  constructor(name) {
    super(name);
  }
}

let a = new Animal('Jack');

// index.ts(7,19): TS2675: Cannot extend a class 'Animal'. Class constructor is marked as private.
// index.ts(13,9): TS2673: Constructor of class 'Animal' is private and only accessible within the class declaration.

当构造函数修饰为 protected 时,该类只允许被继承:

class Animal {
  public name;
  protected constructor(name) {
    this.name = name;
  }
}
class Cat extends Animal {
  constructor(name) {
    super(name);
  }
}

let a = new Animal('Jack');

// index.ts(13,9): TS2674: Constructor of class 'Animal' is protected and only accessible within the class declaration.

参数属性§

// console.log(4/[]);
class Person {
    public name:String;
    public age:Number;
    private sex:String;
    protected score:Number;

    public constructor(name:String,age:Number,sex:String,score:number) {
        this.name = name;
        this.age = age;
        this.sex = sex;
        this.score = score;
    }
    public sayHi() {
        console.log('hello '+this.name);
    }
}
let person = new Person("张三",25,"male",100);
person.sayHi();
// let sex = person.sex;
class Student extends Person {
    constructor(name:String,age:Number,sex:String,score:number) {
        super(name,age,sex,score);
    }
    getScore() {
        console.log(this.name +"得了"+this.score);
    }
}

let stu = new Student("ljb",26,"male",150);
stu.getScore();

修饰符和readonly还可以使用在构造函数参数中,等同于类中定义该属性同时给该属性赋值,使代码更简洁。只读属性,但是在构造函数中是可以修改的。

class Animal {
  // public name: string;
  public constructor(public name) {
    // this.name = name;
  }
}

readonly§

只读属性关键字,只允许出现在属性声明或索引签名或构造函数中。

class Animal {
  readonly name;
  public constructor(name) {
    this.name = name;
  }
}

let a = new Animal('Jack');
console.log(a.name); // Jack
a.name = 'Tom';

// index.ts(10,3): TS2540: Cannot assign to 'name' because it is a read-only property.

注意如果 readonly 和其他访问修饰符同时存在的话,需要写在其后面。

class Animal {
  // public readonly name;
  public constructor(public readonly name) {
    // this.name = name;
  }
}

class X {
    // readonly age:number;
    constructor(readonly age:number) {
        //readonly以及三个修饰符定义在参数上,那就是创建并初始化参数,
        // 有了这些修饰符都可以不用写这些赋值的操作了

        this.age = age;
    }
    play() {
        // this.age = 20; 报错 只读属性不能修改
    }
}

抽象类§

abstract 用于定义抽象类和其中的抽象方法。

什么是抽象类?

首先,抽象类是不允许被实例化的:

abstract class Animal {
  public name;
  public constructor(name) {
    this.name = name;
  }
  public abstract sayHi();
}

let a = new Animal('Jack');

// index.ts(9,11): error TS2511: Cannot create an instance of the abstract class 'Animal'.

上面的例子中,我们定义了一个抽象类 Animal,并且定义了一个抽象方法 sayHi。在实例化抽象类的时候报错了。

其次,抽象类中的抽象方法必须被子类实现:

abstract class Animal {
  public name;
  public constructor(name) {
    this.name = name;
  }
  public abstract sayHi();
}

class Cat extends Animal {
  public eat() {
    console.log(`${this.name} is eating.`);
  }
}

let cat = new Cat('Tom');

// index.ts(9,7): error TS2515: Non-abstract class 'Cat' does not implement inherited abstract member 'sayHi' from class 'Animal'.

上面的例子中,我们定义了一个类 Cat 继承了抽象类 Animal,但是没有实现抽象方法 sayHi,所以编译报错了。

下面是一个正确使用抽象类的例子:

abstract class Animal {
  public name;
  public constructor(name) {
    this.name = name;
  }
  public abstract sayHi();
}

class Cat extends Animal {
  public sayHi() {
    console.log(`Meow, My name is ${this.name}`);
  }
}

let cat = new Cat('Tom');

上面的例子中,我们实现了抽象方法 sayHi,编译通过了。

需要注意的是,即使是抽象方法,TypeScript 的编译结果中,仍然会存在这个类,上面的代码的编译结果是:

var __extends =
  (this && this.__extends) ||
  function (d, b) {
    for (var p in b) if (b.hasOwnProperty(p)) d[p] = b[p];
    function __() {
      this.constructor = d;
    }
    d.prototype = b === null ? Object.create(b) : ((__.prototype = b.prototype), new __());
  };
var Animal = (function () {
  function Animal(name) {
    this.name = name;
  }
  return Animal;
})();
var Cat = (function (_super) {
  __extends(Cat, _super);
  function Cat() {
    _super.apply(this, arguments);
  }
  Cat.prototype.sayHi = function () {
    console.log('Meow, My name is ' + this.name);
  };
  return Cat;
})(Animal);
var cat = new Cat('Tom');

类的类型§

给类加上 TypeScript 的类型很简单,与接口类似:

class Animal {
  name: string;
  constructor(name: string) {
    this.name = name;
  }
  sayHi(): string {
    return `My name is ${this.name}`;
  }
}

let a: Animal = new Animal('Jack');
console.log(a.sayHi()); // My name is Jack
abstract class Y {
    abstract name:String;
    abstract sayHi();//不能有具体的实现
}

class Z extends Y{
    name:String;
    constructor(name:String) {
        super();
        this.name = name;
    }
    //在子类中具体去实现
    sayHi() {
        console.log('hi');
    }
}
const z1 = new Z("ljb");
console.log(z1.name);
z1.sayHi()

类与接口

之前学习过,接口(Interfaces)可以用于对「对象的形状(Shape)」进行描述。

这一章主要介绍接口的另一个用途,对类的一部分行为进行抽象。

类实现接口§

实现(implements)是面向对象中的一个重要概念。一般来讲,一个类只能继承自另一个类,有时候不同类之间可以有一些共有的特性,这时候就可以把特性提取成接口(interfaces),用 implements 关键字来实现。这个特性大大提高了面向对象的灵活性。

举例来说,门是一个类,防盗门是门的子类。如果防盗门有一个报警器的功能,我们可以简单的给防盗门添加一个报警方法。这时候如果有另一个类,车,也有报警器的功能,就可以考虑把报警器提取出来,作为一个接口,防盗门和车都去实现它:

interface Alarm {
    alert(): void;
}

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(): void;
}

interface Light {
    lightOn(): void;
    lightOff(): void;
}

class Car implements Alarm, Light {
    alert() {
        console.log('Car alert');
    }
    lightOn() {
        console.log('Car light on');
    }
    lightOff() {
        console.log('Car light off');
    }
}

接口继承接口§

接口与接口之间可以是继承关系:

interface Alarm {
    alert(): void;
}

interface LightableAlarm extends Alarm {
    lightOn(): void;
    lightOff(): void;
}

这很好理解,LightableAlarm 继承了 Alarm,除了拥有 alert 方法之外,还拥有两个新方法 lightOn 和 lightOff

上例中,Car 实现了 Alarm 和 Light 接口,既能报警,也能开关车灯。

接口继承类§

常见的面向对象语言中,接口是不能继承类的,但是在 TypeScript 中却是可以的:

class Point {
    x: number;
    y: number;
    constructor(x: number, y: number) {
        this.x = x;
        this.y = y;
    }
}

interface Point3d extends Point {
    z: number;
}

let point3d: Point3d = {x: 1, y: 2, z: 3};

为什么 TypeScript 会支持接口继承类呢?

实际上,当我们在声明 class Point 时,除了会创建一个名为 Point 的类之外,同时也创建了一个名为 Point 的类型(实例的类型)。

所以我们既可以将 Point 当做一个类来用(使用 new Point 创建它的实例):

class Point {
    x: number;
    y: number;
    constructor(x: number, y: number) {
        this.x = x;
        this.y = y;
    }
}

const p = new Point(1, 2);

也可以将 Point 当做一个类型来用(使用 : Point 表示参数的类型):

class Point {
    x: number;
    y: number;
    constructor(x: number, y: number) {
        this.x = x;
        this.y = y;
    }
}

function printPoint(p: Point) {
    console.log(p.x, p.y);
}

printPoint(new Point(1, 2));

这个例子实际上可以等价于:

class Point {
    x: number;
    y: number;
    constructor(x: number, y: number) {
        this.x = x;
        this.y = y;
    }
}

interface PointInstanceType {
    x: number;
    y: number;
}

function printPoint(p: PointInstanceType) {
    console.log(p.x, p.y);
}

printPoint(new Point(1, 2));

上例中我们新声明的 PointInstanceType 类型,与声明 class Point 时创建的 Point 类型是等价的。

所以回到 Point3d 的例子中,我们就能很容易的理解为什么 TypeScript 会支持接口继承类了:

class Point {
    x: number;
    y: number;
    constructor(x: number, y: number) {
        this.x = x;
        this.y = y;
    }
}

interface PointInstanceType {
    x: number;
    y: number;
}

// 等价于 interface Point3d extends PointInstanceType
interface Point3d extends Point {
    z: number;
}

let point3d: Point3d = {x: 1, y: 2, z: 3};

当我们声明 interface Point3d extends Point 时,Point3d 继承的实际上是类 Point 的实例的类型。

换句话说,可以理解为定义了一个接口 Point3d 继承另一个接口 PointInstanceType

所以「接口继承类」和「接口继承接口」没有什么本质的区别。

值得注意的是,PointInstanceType 相比于 Point,缺少了 constructor 方法,这是因为声明 Point 类时创建的 Point 类型是不包含构造函数的。另外,除了构造函数是不包含的,静态属性或静态方法也是不包含的(实例的类型当然不应该包括构造函数、静态属性或静态方法)。

换句话说,声明 Point 类时创建的 Point 类型只包含其中的实例属性和实例方法:

class Point {
    /** 静态属性,坐标系原点 */
    static origin = new Point(0, 0);
    /** 静态方法,计算与原点距离 */
    static distanceToOrigin(p: Point) {
        return Math.sqrt(p.x * p.x + p.y * p.y);
    }
    /** 实例属性,x 轴的值 */
    x: number;
    /** 实例属性,y 轴的值 */
    y: number;
    /** 构造函数 */
    constructor(x: number, y: number) {
        this.x = x;
        this.y = y;
    }
    /** 实例方法,打印此点 */
    printPoint() {
        console.log(this.x, this.y);
    }
}

interface PointInstanceType {
    x: number;
    y: number;
    printPoint(): void;
}

let p1: Point;
let p2: PointInstanceType;

上例中最后的类型 Point 和类型 PointInstanceType 是等价的。

同样的,在接口继承类的时候,也只会继承它的实例属性和实例方法。

声明合并

如果定义了两个相同名字的函数、接口或类,那么它们会合并成一个类型:

函数的合并§

之前学习过,我们可以使用重载定义多个函数类型:

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('');
    }
}

接口的合并§

接口中的属性在合并时会简单的合并到一个接口中:

interface Alarm {
    price: number;
}
interface Alarm {
    weight: number;
}

相当于:

interface Alarm {
    price: number;
    weight: number;
}

注意,合并的属性的类型必须是唯一的

interface Alarm {
    price: number;
}
interface Alarm {
    price: number;  // 虽然重复了,但是类型都是 `number`,所以不会报错
    weight: number;
}
interface Alarm {
    price: number;
}
interface Alarm {
    price: string;  // 类型不一致,会报错
    weight: number;
}

// index.ts(5,3): error TS2403: Subsequent variable declarations must have the same type.  Variable 'price' must be of type 'number', but here has type 'string'.

接口中方法的合并,与函数的合并一样:

interface Alarm {
    price: number;
    alert(s: string): string;
}
interface Alarm {
    weight: number;
    alert(s: string, n: number): string;
}

相当于:

interface Alarm {
    price: number;
    weight: number;
    alert(s: string): string;
    alert(s: string, n: number): string;
}

类的合并§

类的合并与接口的合并规则一致。

泛型

泛型(Generics)是指在定义函数、接口或类的时候,不预先指定具体的类型,而在使用的时候再指定类型的一种特性。

简单的例子§

首先,我们来实现一个函数 createArray,它可以创建一个指定长度的数组,同时将每一项都填充一个默认值:

function createArray(length: number, value: any): Array<any> {
    let result = [];
    for (let i = 0; i < length; i++) {
        result[i] = value;
    }
    return result;
}

createArray(3, 'x'); // ['x', 'x', 'x']

上例中,我们使用了之前提到过的数组泛型来定义返回值的类型。

这段代码编译不会报错,但是一个显而易见的缺陷是,它并没有准确的定义返回值的类型:

Array<any> 允许数组的每一项都为任意类型。但是我们预期的是,数组中每一项都应该是输入的 value 的类型。

这时候,泛型就派上用场了:

function createArray<T>(length: number, value: T): Array<T> {
    let result: T[] = [];
    for (let i = 0; i < length; i++) {
        result[i] = value;
    }
    return result;
}

createArray<string>(3, 'x'); // ['x', 'x', 'x']

上例中,我们在函数名后添加了 <T>,其中 T 用来指代任意输入的类型,在后面的输入 value: T 和输出 Array<T> 中即可使用了。

接着在调用的时候,可以指定它具体的类型为 string。当然,也可以不手动指定,而让类型推论自动推算出来:

function createArray<T>(length: number, value: T): Array<T> {
    let result: T[] = [];
    for (let i = 0; i < length; i++) {
        result[i] = value;
    }
    return result;
}

createArray(3, 'x'); // ['x', 'x', 'x']

多个类型参数§

定义泛型的时候,可以一次定义多个类型参数:

function swap<T, U>(tuple: [T, U]): [U, T] {
    return [tuple[1], tuple[0]];
}

swap([7, 'seven']); // ['seven', 7]

上例中,我们定义了一个 swap 函数,用来交换输入的元组。

泛型约束§

在函数内部使用泛型变量的时候,由于事先不知道它是哪种类型,所以不能随意的操作它的属性或方法:

function loggingIdentity<T>(arg: T): T {
    console.log(arg.length);
    return arg;
}

// index.ts(2,19): error TS2339: Property 'length' does not exist on type 'T'.

上例中,泛型 T 不一定包含属性 length,所以编译的时候报错了。

这时,我们可以对泛型进行约束,只允许这个函数传入那些包含 length 属性的变量。这就是泛型约束:

interface Lengthwise {
    length: number;
}

function loggingIdentity<T extends Lengthwise>(arg: T): T {
    console.log(arg.length);
    return arg;
}

上例中,我们使用了 extends 约束了泛型 T 必须符合接口 Lengthwise 的形状,也就是必须包含 length 属性。

此时如果调用 loggingIdentity 的时候,传入的 arg 不包含 length,那么在编译阶段就会报错了:

interface Lengthwise {
    length: number;
}

function loggingIdentity<T extends Lengthwise>(arg: T): T {
    console.log(arg.length);
    return arg;
}

loggingIdentity(7);

// index.ts(10,17): error TS2345: Argument of type '7' is not assignable to parameter of type 'Lengthwise'.

多个类型参数之间也可以互相约束:

function copyFields<T extends U, U>(target: T, source: U): T {
    for (let id in source) {
        target[id] = (<T>source)[id];
    }
    return target;
}

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

copyFields(x, { b: 10, d: 20 });

上例中,我们使用了两个类型参数,其中要求 T 继承 U,这样就保证了 U 上不会出现 T 中不存在的字段。

泛型接口§

之前学习过,可以使用接口的方式来定义一个函数需要符合的形状:

interface SearchFunc {
  (source: string, subString: string): boolean;
}

let mySearch: SearchFunc;
mySearch = function(source: string, subString: string) {
    return source.search(subString) !== -1;
}

当然也可以使用含有泛型的接口来定义函数的形状:

interface CreateArrayFunc {
    <T>(length: number, value: T): Array<T>;
}

let createArray: CreateArrayFunc;
createArray = function<T>(length: number, value: T): Array<T> {
    let result: T[] = [];
    for (let i = 0; i < length; i++) {
        result[i] = value;
    }
    return result;
}

createArray(3, 'x'); // ['x', 'x', 'x']

进一步,我们可以把泛型参数提前到接口名上:

interface CreateArrayFunc<T> {
    (length: number, value: T): Array<T>;
}

let createArray: CreateArrayFunc<any>;
createArray = function<T>(length: number, value: T): Array<T> {
    let result: T[] = [];
    for (let i = 0; i < length; i++) {
        result[i] = value;
    }
    return result;
}

createArray(3, 'x'); // ['x', 'x', 'x']

注意,此时在使用泛型接口的时候,需要定义泛型的类型。

泛型类§

与泛型接口类似,泛型也可以用于类的类型定义中:

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; };

泛型参数的默认类型§

在 TypeScript 2.3 以后,我们可以为泛型中的类型参数指定默认类型。当使用泛型时没有在代码中直接指定类型参数,从实际值参数中也无法推测出时,这个默认类型就会起作用。

function createArray<T = string>(length: number, value: T): Array<T> {
    let result: T[] = [];
    for (let i = 0; i < length; i++) {
        result[i] = value;
    }
    return result;
}