【TS02】一文搞懂Typescript基础&高级语法

285 阅读14分钟

D-Chat_20220623161641.png

前言

2020年年进入团队以来,团队所有的项目都转向Typescript。在这期间,做过很多尝试,也阅读过一些优质的文章和源码。现如今,大多数开源项目都将Typescript做为开发的主力军。

跟往常学习技术一样,我查阅的大多数文章都是在进行一个Typescript的基础使用,涉及实践应用这一块少之又少,而我一向的理念是,尽量站在巨人的肩膀上再深入,后人尽最大可能利用好前人的经验。

本篇文章就从项目实践的角度来聊聊,针对每一个TS常用语法,探讨下Typescript在真实项目中开发的实践心得和开发体验。

最后,我建议大家在技术选型时,先思考团队是否需要Typescript,以及Typescript是否可以解决当前项目生产的困境。

Typescript是什么?

image.png

要回答这个问题先要清晰Javascript的定位

  • Javascript是一种解释型的函数式编程语言,并不是一种面向对象的语言。
  • 动态编程语言,只有在运行时才清楚具体情况。

Typescript定位:

  • JS的一个超集,面向对象,扩展了JS的语法;
  • 自成文档,方便review、重构等;
  • JS执行前对其进行类型检查,TS+JS 模拟了 编译型语言:(1)变量初始化;(2)上下文推断;
  • 降低搭建环境的成本,省去babel等研究时间;

数据类型

  1. string
  2. number(可以是二进制0B**、10进制、16进制0X**)
  3. boolean
  4. null 和 undefined
  5. void
  6. tuple元组
  7. array
  8. enum
  9. never
  10. any
  11. object
  12. unknown

1.基本数据类型

string、number、boolean,基本类型是小写;
String、Number、Boolean,基本包装类型,首字母大写。

注意:
基础类型数据可以赋值给 基本包装类型声明的变量;
但,基本包装类型的数据不能赋值给 基础数据类型声明的变量。

✅ let s: String = 'miaov'根据【1】此处声明可以是string或者String
❎ let s: string = new String('miaov')根据【2】此处只能声明String

image.png

2.数组Array类型

数组是一个集合,我们还需要指定在数组中的元素的类型。

ts中常用的定义数组有两种方式,我们通过以下语法为数组内的元素指定类型
(法1) Array< type > ## 数组范型
(法2) type[]
(法3) 接口类型声明
interface ArrayNumber {
[index: number]: number
}
let arrayNumberInterface: ArrayNumber = [1, 1, 2, 3, 5];

联合类型声明:

let arrayFibonacci3: (number | string | boolean)[] = [1, 1, '2', false, 5, 8];

3.元组tuple

元组和数组结构类似,元组类型(tuple)属于数组的一种.可以理解成枚举类型数组

let arr : [string, number];
arr = ['pr', 30];

直接对变量类型定义并赋值的时候,不能多也不能少 ,顺序要对应, undefined是可以赋值的。

那仔细思考的同学一定会问一个问题,如果越界怎么办?声明了两个Item,但处理了第三个item。

Typescript 3.1之后访问越界元素会报错, 包括了读 和 写。

image.png

4.Enums

enum 枚举名 {        
       标识符[=整型常数],         
       标识符[=整型常数], 
          ... 
       标识符[=整型常数], 
}; 

//实例
enum Flag {success=1, error=2};
let s:Flag = Flag.success;

枚举默认值

enum Role={ X }, 枚举的默认初始值是0
可以调整一开始的范围:
enum Role { Employee = 3, Manager, Admin }
它不带有初始化器且它之前的枚举成员是一个数字常量。 这种情况下,当前枚举成员的值为它上一个枚举成员的值加1

反查枚举name

Role[3]

5. Any

any 是默认的类型,其类型的变量允许任何类型的值

6. Void

定义为void的变量可以给其赋值 undefined、null(非严格模式下,严格模式编译报错),在 TypeScirpt 中,可以用 void 表示没有任何返回值的函数.

# 一个方法没有返回值
function alertName(): void {
  console.log('My name is muyy')
}
必须是没有return,只要有return,即使是return undefined,也不能指定void,编译一定会报错的。

7. Null & Undefined

非严格模式下
undefined和null可以赋值给任一类型声明的变量
let val:number = undefined
let n1:undefined = null

严格模式下
定义为null的变量只能给其赋值null,定义为undefined的变量只能显式给其赋值undefined

image.png

8. Never

表示永不存在的值的类型,返回 never 的函数必须存在无法达到的终点。

使用场景有

  • 1)抛出异常;
  • 2)不会有返回值的函数表达式或箭头函数表达式的返回值类型;
  • 3)变量;
# 抛出错误
function error(message: string): never {
    throw new Error(message);
}
// 推断的返回值类型为 never
function fail() {
    return error("有错");
}
# 死循环
// 返回 never 的函数必须存在无法达到的终点
function infiniteLoop(): never {
    while (true) {
        // doSomething
    }
}

9.联合属性

联合类型表示取值可以为多种类型中的一种,各个类型间使用分隔线|连接

let strOrNumValue: string | number = 'test';

当一个属性是联合属性,但两个声明中有不同的子属性、子方法,怎么处理?

当一个变量定义为联合类型,我们只能访问联合类型的公共方法和属性

function getLength(something: string | number) {
    return something.length; 
    // 报错,number 类型不包含 length 属性
}
function myToString(something: string | number) {
    return something.toString(); 
    // 正确,number 和 string 类型都包含 toString 方法
}

联合类型的变量在被赋值后,会根据类型推论的规则推导出一个类型

let something: string | number;
something = 'Cooper';
console.log(something.length) 
// something 赋值字符串后,被推导为 string 类型,因而可以访问 length 属性
something = 3
console.log(something.length) // 报错

10. object & Object & {}

object 是TypeScript v2.2引入的一种非基本类型,不能被赋予原始值。 Object 是对TypeScript对JavaScript Object.prototype原型对象的定义,是所属对象类型的顶层类型,即所有对象类型都继承了Object中定义的属性/方法。同时,由于JavaScript的拆箱装箱机制,Object类型的变量可以被赋予原始值,而基本类型也可以访问Object中定义的属性/方法。 {} 是一个没有任何成员的对象类型,它可以访问Object中定义的属性/方法,也可以被赋予原始值。

因此,在约束对象类型时,我们应该始终使用object!

let objs08:object = null//error
let objs09:Object = null//error
let objs10:object = undefined//error
let objs11:Object = undefined//error

11. 内置对象

JavaScript 中有很多定义好的对象,我们可以当作定义好的类型来使用。

let bodyDoc: HTMLDocument = document.body; // html文档流

  • HTMLElement、
  • NodeList 、
  • MouseEvent等、
  • Boolean等对象

12. 字符串字面量类型

字符串字面量类型用来约束变量取值只能是某几个字符串取值中一个,作为类型。

type eventName = 'click'| 'scroll' | 'mousemove' 作为类型

type eventName = 'click' |  'scroll' | 'mousemove'  作为类型
// 第二个参数 event 只能是'click' |  'scroll' | 'mousemove'三个中的某一个
function handleEvent(ele: Element, event: EventNames) {
    // do something
}
handleEvent(doument.getELementById('hello'), 'scroll'); // true
handleEvent(doument.getELementById('hello'), 'dbclick'); // error

13.交叉数据类型

多个类型的叠加,并且的关系 ,多种接口类型成员都要拥有(接口)

举例:let v:string&number&boolean 同时符合这三种类型,开玩笑啦,意思是这么个意思。

image.png

14. 类类型 {new()}

let c: {new( )} //c就是一个可以产生对象的构造函数,告诉ts 接收的是一个构造函数

image.png

Function getPeisonObj( c:{new() :Person}){
    xxx
}

该构造函数new后返回一个Person类型的实例。

c:{new() :Person}c:new()=>Person 是一样的,后者是前者的简写,意即C的类型是对象类型且这个对象包含返回类型是Person的构造函数。

类型断言

类型断言即手动(或者说是强制)指定一个变量的类型,从该变量给出的类型中指定一个类型.

注意:类型断言不是类型转换,断言一个联合类型中不存在的类型将会报错

法1:<类型>变量, < string > a
法2:变量 as 类型 // tsx 中仅支持此种方式, a as string

函数

1. 函数的定义

函数声明法,指定返回return的类型,以及传入参数的类型\

function getInfo(name:string,age:number):string{
    return `${name} --- ${age}`;
}

函数表达式

const sum4: (x: number, y: number) => number = function(x: number, y: number): number {
    return x + y;
}

接口形式

    (x: string, y: string): boolean
   }
   let function5: Function5 = (x: string, y: string) => {
     return x.search(y) > -1;
   }

2. 可选参数

ts中实参和形参必须一样,如果不一样就需要配置可选参数。在 TypeScript 里我们可以在参数名旁使用?实现可选参数的功能。可选参数默认为 undefined

function getInfo(name:string,age?:number):string{
         if(age){
            return `${name} --- ${age}`;
         }else{
           return `${name} ---年龄保密`;
       }
}

关键点:
!!!可选参数默认undefined
并且,可选参数必须配置到所有参数的最后面,包括默认参数。

3. 默认参数

es6和ts中都可以设置默认参数.

function getInfo(name:string,age:number=20):string{
         if(age){
              return `${name} --- ${age}`;
         }else{
              return `${name} ---年龄保密`;
         }
}

4. 剩余参数(扩展运算符)

function sum(a:number,b:number,...result:number[]):number{
       var sum=a+b;
       for(var i=0;i<result.length;i++){
            sum+=result[i];  
       }
      return sum;
}

5. 函数重载

要用同名函数实现不同功能,函数的名称相同,但参数个数、类型、顺序不同。 注:与返回值无关

java中方法的重载:重载指的是两个或者两个以上同名函数,但它们的参数不一样,这时会出现函数重载的情况。
typescript中的重载:通过为同一个函数提供多个函数类型定义来试下多种功能的目的。ts为了兼容es5 以及 es6 重载的写法和java中有区别。

es5中出现同名方法,函数声明提升,后面的会覆盖前面的。

ts中的重载:

step1 先声明两个同名的方法,注意无方法体

function getInfo(name:string):string;    //注意无方法体 
function getInfo(age:number):string;

step2 定义函数,根据穿参不同进行区分

function getInfo(str:any):any{   //都是any
   if(typeof str==='string'){  //根据传入参数的类型进行判断究竟是怎么处理
       return '我叫:'+str;
   }else{
       return '我的年龄是'+str;
   }
}
// alert(getInfo('张三'));   //正确
// alert(getInfo(20));   //正确

6. 函数表达式中this的问题

分析案例: image.png

问题: 接口中1处 定义声明了 onclick()方法的this(伪参数)是指向void的, 但是,3处所调用的方法,在2处指向了Handler的实例,所以编译会报错。

方案:

将2处的this指向void即可。this.type也随之不可以使用,因为this是void
默认情况下,ts中函数的this 指向:any,(并不是所有,比如事件函数)

配置:--noImplicitThis
我们可以通过配置文件tsconfig.json中配置 --noImplicitThis 选项来取消默认 this 的 any 类型设置this参数,我们可以在函数参数中提供一个显示的 this 参数,this 参数是一个假的参数(给ts看的),它出现在参数列表的最前面,运行过程中该参数并不存在,是给ts检测用的,希望ts按照参数指定的this去做检测。

1. ts中类的定义

ts中定义类与es6 相似,无非多了属性及方法参数的类型说明,以及public,private,protected。

Ts中的类,成员属性必须要声明后使用,并且类的成员属性不是在构造函数中进行声明的,construtor前已经声明

例:
 class Person{
        # 属性
        name:string; 
        // !!!!!!!!属性类型声明,必须  
        // 这个是对后文this.name类型的声明
        // 提倡把全部全局变量先在前面声明好。
        constructor(username: string) {
            this.name = username //给声明过的属性赋值
        }
        
        # 方法
         getName():string{
            return this.name;
        }
        setName(name:string):void{
            this.name=name;
        }

}
实例化:var p=new Person('张三');

等价于在constructor中定义时直接在变量前加public

 class Person{
        constructor(public name: string) {
            // constuctor public name  相当于 this.name = name
        }
}

在构造函数的参数上使用public等同于创建了同名的成员变量。

constructor(public firstName, public middleInitial, public lastName) {
编译结果:
var Student = /** @class */ (function () {
    function Student(firstName, middleInitial, lastName) {
        this.firstName = firstName;
        this.middleInitial = middleInitial;
        this.lastName = lastName;
        this.fullName = firstName + " " + middleInitial + " " + lastName;
    }
    return Student;
}());

2. ts中实现继承

ts实现继承的方式与es6相同,extends。

class Web extends Person{
     constructor(name:string){
         super(name);  
         /*初始化父类的构造函数*/表示调用父类的构造函数,在父类的实例基础上实例化
         }
     }
     var w=new Web('李四');

继承内置TS对象时存在的坑 image.png

image.png

在构造函数中配置 Object.setPrototypeOf(this, 当前类.prototype)
eg. FooError.prototype

3. ts类中的修饰符

typescript里面定义属性的时候给我们提供了三种修饰符:

public (默认):公有 在当前类里面、子类、类外面(实例)都可以访问
protected:保护类型 在当前类里面、子类里面可以访问,在类外部(实例)没法访问
private :私有 在当前类里面可以访问,子类、类外部(实例)都没法访问

私有数据想被外界访问可以通过getter、setter存取器跨接 属性如果不加修饰符 默认就是 公有 (public)

接下来,代码实例看下这三部分(类里面、子类、类外面)访问的区别:

类中访问

class Person{
   public name:string;  /*公有属性*/
   constructor(name:string){
       this.name=name;
   }
   run():string{
       return `${this.name}在运动`    //此处为当前类中访问
   }
}

在子类中访问

     class Web extends Person{
           constructor(name:string){
                super(name);  /*初始化父类的构造函数*/
           }
           run():string{
                return `${this.name}在运动-子类`   //此处为在子类中访问
           }
           work(){
                alert(`${this.name}在工作`)
           }
    }

在类外(实例中)访问

 var  p=new Person('哈哈哈');  
 alert(p.name);  //创建实例后在外面直接访问为在类外访问

4. 存取器跨接private

TS 支持 getters/setters 来截取对对象成员的访问 使用存取器的成员: private 修饰的私有数据
编译目标为 ES5+, 只有 get的存取器自动被推断为 readonly。

老规矩,代码示例看下:

Class Person{
    private _age :number = 10
    //存取器,这个age并不会作为方法,而是作为属性去访问
    Get age():number{
        Return this._age
    }
    Set age(age:number){
        If(age>0&&age<150){
            This._age = age
        }
    }
}


Let p1:Person = new Person();
Console.log(p1.age)   //  10

注意,直接读取的是存取器,并且,类外是可以访问的。

5. 静态属性,静态方法

静态方法中只能调用静态属性,用 static修饰。

class Per{
     //静态属性
        static sex="男";
        static print(){  
     // 静态方法里面没法直接调用类里面的属性,只能调用静态属性
            alert('print方法'+Per.sex);
        }
}

6. 多态

父类定义一个方法不去实现,让继承它的子类去实现,每一个子类有不同的表现,多态属于继承,可以理解为定义动作,但没定义动作实现细节。 代码实例来理解:

class Animal {
      name:string;
      constructor(name:string) {
           this.name=name;
      }
      eat(){ 
          // 定义动作
         //具体吃什么不知道,具体吃什么由继承它的子类去实现 ,每一个子类的表现不一样
                        console.log('吃的方法')
      }
}

接下来看具体多态的代码实现

class Dog extends Animal{
         constructor(name:string){
              super(name)
              Object.setPrototypeOf( this, xxx.prototype )
         }
         eat(){
              return this.name+'吃粮食'
         }
}
class Cat extends Animal{
        constructor(name:string){
              super(name)
        }
        eat(){
             return this.name+'吃老鼠'
        }
 }

7. 抽象类&抽象方法

Typescript有抽象类的概念,它是供其他类继承的基类不能直接被实例化
不同于接口,抽象类必须包含一些抽象方法,同时也可以包含非抽象的成员
抽象类中的抽象方法必须在派生类中实现。继承了抽象类的子类必须实现了所有抽象方法才能被实例化,否则该子类也必须声明为抽象的。但是,多态方法如run()则不做此要求。

typescript中的抽象类:它是提供其他类继承的基类,不能直接被实例化。

abstract关键字定义抽象类和抽象方法,abstract 关键字可以与 修饰符一起使用。 抽象类中的抽象方法不包含具体实现并且必须在派生类中实现abstract抽象方法只能放在抽象类里面拥有抽象方法的类必须是抽象类但是抽象类不一定拥有抽象方法,抽象类中也可以包含有具体细节的方法。

拥有抽象方法的一定是抽象类,抽象类不一定拥有抽象方法, 派生类中必须实例化所有抽象方法,一个都不能差。抽象方法不包含具体实现,有点类似函数重载的声明方式。

抽象类和抽象方法用来定义标准
标准:Animal 这个类要求它的子类必须包含eat方法

abstract class Animal{
    public name:string;
    constructor(name:string){
        this.name=name;
    }
    abstract eat():any;  //抽象方法不包含具体实现并且必须在派生类中实现。
    run(){ // 多态
        console.log('其他方法可以不在派生类不实现')
    } 
    //抽象类中的抽象方法必须在派生类中实现,但是,多态方法如run()则不做此要求。
}
// var a=new Animal() /*错误的写法, 抽象类不能实例*/
class Dog extends Animal{
    //抽象类的子类必须实现抽象类里面的抽象方法
    constructor(name:any){
        super(name)
    }
    eat(){   //抽象方法必须在子类中重新实现
        console.log(this.name+'吃粮食')
    }
}
var d=new Dog('小花花');
d.eat();
class Cat extends Animal{
    //抽象类的子类必须实现抽象类里面的抽象方法
    constructor(name:any){
        super(name)
    }
    run(){
    }
    eat(){

        console.log(this.name+'吃老鼠')
    }
    
}
var c=new Cat('小花猫');
c.eat();

8.接口形式来定义新类

8.1 类implements接口的实现

通常,一个类只继承另一个类。有时,不同类之间有一些共有的特性,把这些特性提取出来可以提高效率,提取出来的就是接口,用关键字 implements 标识, 类似抽象类,这些共有的特性必须全部在 派生类中全部实例化

interface IPerson {
    readonly id: number;
    name: string
}
# 共有特性
// 拍照
interface Photo {
    photo( ): string;
}
// 闪光灯
interface Lamp {
    lampOn(): void;
    lampOff(): void;
}
// 手机超类
class Phone {}
// 手机派生类
class HuaweiPhone extends Phone implements Photo, Lamp {  //需要在派生类中实现
    photo(): string {
        return '华为拍照';
    }
    lampOff() {}
    lampOn(){}
    //implements 了interface后,需要把接口中的方法都要定义一下,且要符合返回类型要求,  否则会报错
}

// 数码相机  测试2
class DigitalCamera implements Photo, Lamp {
    photo(): string {
        console.log('数码拍照')
    }
}

测试2 会报错在于 DigitalCamera 实现了接口 Lamp,可却没有定义里面的方法,必须重新定义里面的每个方法。 接口 Phone 中 photo 需要返回 string,可是类 DigitalCamera 中的 phone 没有返回值

8.2 接口继承类

接口继承了一个类类型时,它会继承类的成员但不包括其实现。
就好像接口声明了所有类中存在的成员,但并没有提供具体实现一样。
接口同样会继承到类的private和protected成员。 这意味着当你创建了一个接口继承了一个拥有私有或受保护的成员的类时,这个接口类型只能被这个类或其子类所实现(implement)。

class Control {
    private state: any;
}
interface SelectableControl extends Control {
    select(): void;
}

// 一个错误case
class Image implements SelectableControl {// 错误:“Image”类型缺少“state”属性。
    select() { }
}

在上面的例子里,SelectableControl包含了Control的所有成员,包括私有成员state。 因为 state是私有成员,所以只能够是Control的子类们才能实现SelectableControl接口。 因为只有 Control的子类才能够拥有一个声明于Control的私有成员state,这对私有成员的兼容性是必需的。

接口

面向对象语言中,接口(Interfaces)是对行为的抽象(可以用事物的本质来辅助理解)。而具体如何行动由类(class)来实现(implement)(可以用事物的现象来辅助理解),接口中定义的规则是抽象的,不能有具体的值与实现

接口的作用:在面向对象的编程中,接口是一种规范的定义,它定义了行为和动作的规范,在程序设计里面,接口起到一种限制和规范的作用。接口的作用就是为这些类型命名和为你的代码或第三方代码定义结构——即,ts按此接口结构检测数据。接口定义了某一批类所需要遵守的规范,接口不关心这些类的内部状态数据,也不关心这些类里方法的实现细节,它只规定这批类里必须提供某些方法(类似abstract类以及多态),提供这些方法的类就可以满足实际需要。 类型检查器会检查变量是否符合接口定义的结构,类型检查器只会检查必须的属性是否存在,以及类型是否匹配,类型检查器不会检查属性的顺序。

0. 纯interface文件编译

image.png 编译结果,空文件(空模块)

image.png

1. 属性类接口

对批量方法传入参数进行约束。

interface FullName{
    firstName:string  ;   //注意必须以 ; 结束
    secondName:string;
}
function printName(name:FullName){
    // 传入对象 中必须有  firstName  secondName
    console.log(name.firstName+'--'+name.secondName);
}
printName('1213');  //错误

var obj={   /*传入的参数必须包含 firstName  secondName*/
    age:20,
    firstName:'张',
    secondName:'三'
}; 
printName(obj) // true

//虽然在接口定义的方法属性之外有其他的额外属性age,但这个属性也是不可以被使用的,
可以有额外参数,但是不能被使用。也就是必须按照接口来定义并使用。

参数的顺序可以不一样。

那我们就可以随便多传参数了吗?
不行的。

举个例子来说:

printName({  
            age:20,
            firstName:'张',
            secondName:'三'
        };
)

这种直接传参而非先在外面定义的情况下,要求所传入对象中必须只有interface接口所要求的属性方法。

1.1 可选属性

interface FullName{
   firstName:string;
   secondName?:string;
}

1.2 只读属性

interface Person8 {
    readonly name: string;
}
实例的对象,给name赋值会报错

对只读属性的约束是第一次给只读属性的对象赋值,而不是第一次给只读属性赋值。 就是 第一次接口对应的对象创建时可以赋值,其他时间不可以再次赋值,即使初始时没有给只读属性变量赋值,之后也不可以再次复制。

1.3 任意属性, 索引签名

interface Person8 {
    [propName: string]: any;
}

2. 函数类型接口

对方法传入的参数 以及返回值进行约束;
批量约束;

拿加密实例来看下,interface声明中少了一个函数名称

interface encrypt{
    (key:string,value:string):string; //相比其他只缺个函数名字
}

通过接口的形式来定义函数,函数类型接口并不是定义对象(类)方法。

image.png

函数类型接口不能定义多个方法。

3. 可索引接口(数组、对象的约束)

索引key的类型只能是 number 和 string。

        可索引接口 对数组的约束
                interface UserArr{
                    [index:number]:string
                }
                // var arr:UserArr=['aaa','bbb'];
                // console.log(arr[0]);
                var arr:UserArr=[123,'bbb'];  /*错误*/
                console.log(arr[0]);
        可索引接口 对对象的约束
               interface UserObj{
                    [index:string]:string
                }
                var arr:UserObj={name:'张三'};

 思考:一个非常不错的问题,任意属性如果设置为string,可选属性设置为number,两者会有冲突吗?

interface Person6 {
    name: string;
    age?: number;
    [propName: string]: string;
}

let pr6: Person6 = {
    name: '胖芮',
    age: 30,
    address: '杭州'
}

答案是,会报错。
报错原因 age 赋值既不能为数字也不能为字符串(到底闹哪样,让不让人活了); age 的 number 类型不是任意属性的 string 的子集,所以 age 怎么赋值都不对 所以,我们可以将任意属性改为 any。 这个点,大家还是要格外注意的,很有可能只是因为平时都设置any了,所以没触发出来过。

4. 类类型接口

这部分,也即是上一章节中的接口形式来定义新类,对类的约束 和 抽象类抽象有点相似。

interface Animal{
        name:string;
        eat(str:string):void;
}
class Dog implements Animal{   
//implement指的是实现这个接口,继承接口的类必须拥有接口定义的必须属性或方法
        name:string;
        constructor(name:string){
            this.name=name;
        }
        eat(){
            console.log(this.name+'吃粮食')
        }
    }
    var d=new Dog('小黑');
    d.eat();
    class Cat implements Animal{
        name:string;
        constructor(name:string){
            this.name=name;
        }
        eat(food:string){
            console.log(this.name+'吃'+food);
        }
    }
    var c=new Cat('小花');
    c.eat('老鼠');

implement指的是实现这个接口,继承接口的类必须拥有接口定义的必须属性或方法

5. 接口扩展

继承(implements)接口的类必须拥有接口定义的必须属性或方法,一个类可以实现implememts多个接口,但只能有一个父类,接口之间也可以继承.

interface Animal{
        eat():void;
}

接口继承接口

interface Person extends Animal{  //实现这个接口,就必须实现接口中的两个方法
        work():void;
}

实现这个派生接口就必须实现接口中的两个方法。

类implements接口

class Web implements Person{
        public name:string;
        constructor(name:string){
            this.name=name;
        }
        eat(){
            console.log(this.name+'喜欢吃馒头')
        }
        work(){
            console.log(this.name+'写代码');
        }
}
var w=new Web('小李');
w.eat();

通常,一个类只继承另一个类。有时,不同类之间有一些共有的特性,把这些特性提取出来可以提高效率,提取出来的就是接口,用关键字 implements 标识, 类似抽象类,这些共有的特性必须全部在 派生类中全部实例化

既继承类,又implements接口

    interface Animal{
        eat():void;
    }
    interface Person extends Animal{
        work():void;
    }
    class Programmer{
        public name:string;
        constructor(name:string){
            this.name=name;
        }
        coding(code:string){
            console.log(this.name+code)
        }
    }
    class Web extends Programmer implements Person{  既继承类,又实现接口
        
        constructor(name:string){
           super(name)
        }
        eat(){
            console.log(this.name+'喜欢吃馒头')
        }
        work(){
            console.log(this.name+'写代码');
        }
        
    }
    var w=new Web('小李');
    // w.eat();
    w.coding('写ts代码');

接口继承类

接口继承了一个类类型时,它会继承类的成员但不包括其实现。
就好像接口声明了所有类中存在的成员,但并没有提供具体实现一样。
接口同样会继承到类的private和protected成员。 这意味着当你创建了一个接口继承了一个拥有私有或受保护的成员的类时,这个接口类型只能被这个类或其子类所实现(implement)。

class Control {
    private state: any;
}
interface SelectableControl extends Control {
    select(): void;
}

// 一个错误case
class Image implements SelectableControl {// 错误:“Image”类型缺少“state”属性。
    select() { }
}

泛型

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

通俗理解:泛型就是解决 类 接口 方法的复用性、以及对不特定数据类型的支持(类型校验) 只能返回string类型的数据

泛型会在调用时指定

1. 泛型定义

    function getData1(value:string):string{
        return value;
    }
    function getData2(value:number):number{
        return value;
    }

代码冗余,有没有其他办法解决呢?

函数重载好像是一种方案;

另一种方案,泛型约束

function info<S, N>(name: S, age: N): [S, N] {
    return [name, age];
}
console.log(info('pr', 18)); // [ 'pr', 18 ]

约定S与N为泛型,输出会复用当前的类型,而S、N是在函数调用的时候指定的。

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

2. 泛型函数(类型< T >声明都是在括号前)

泛型:可以支持不特定的数据类型
要求:传入的参数和返回的参数一致
T表示泛型,具体什么类型是调用这个方法的时候决定

泛型函数,无论是哪种函数表现形式,泛型定义<T>都是紧贴参数括号,出现在参数括号前面。

函数声明法

       function getData<T> ( value:T ) :T{
           return value;
       }
       getData<number>(123);  //调用的方法
       getData<string>('1214231');
       getData<number>('2112');       /*错误的写法*/ 

即使定义了泛型也可以对参数及返回值进行类型声明

        function getData<T>(value:T):any{  
        //即使定义了泛型也可以对参数及返回值进行类型声明
           return '2145214214';
       }
       getData<number>(123);  //参数必须是number
       getData<string>('这是一个泛型');

函数表达式

let fn: <T>(arg: T) => T;

依旧在括号前进行声明。

接口型

        interface ConfigFn{   //泛型函数接口定义1
            <T>(value:T):T; //还是在参数括号前进行泛型定义
        }

3. 泛型类

class MinClas<T>{
    public list:T[]=[];  //Array<T>
    add(value:T):void{
        this.list.push(value);
    }

    min():T{        
        var minNum=this.list[0];
        for(var i=0;i<this.list.length;i++){
            if(minNum>this.list[i]){
                minNum=this.list[i];
            }
        }
        return minNum;
    }
}
var m1=new MinClas<number>();

实例化类 并且指定了类的T代表的类型是number

泛型约束

<T extends 类型>, 对泛型T进行约束.

image.png

类类型泛型

Function getPeisonObj( c:{new() :Person}){
    xxx
}

c就是一个可以产生对象的构造函数,告诉ts 接收的是一个构造函数,指定构造函数

c: {new():  Array}

泛型表示:

Function getPeisonObj <T>(c: {new( ): T}){ xxx }

4. 泛型接口

image.png

        interface ConfigFn<T>{   泛型接口定义2
            (value:T):T;
        }   
        var myGetData:ConfigFn<string>=getData;     //函数调用2
        myGetData('20');  /*正确*/
        // myGetData(20)  //错误

注意,指定接口泛型的时机,
var myGetData:ConfigFn< string >=getData;

interface Person {
  name:string;
  age:number;
}
function student<T extends Person>(arg:T):T {
  return arg;
}

student({name:'lili'});//类型 "{ name: string; }" 中缺少属性 "age",但类型 "Person" 中需要该属性
student({ name: "lili" , age:'11'});//不能将类型“string”分配给类型“number”
student({ name: "lili" , age:11});

模块 & 命名空间

“内部模块”现在称做“命名空间”。 “外部模块”现在则简称为“模块”

模块

ES6中引入了模块的概念,在TypeScript中也支持模块的使用。使用import和export关键字来建立两个模块之间的联系。 ts 沿用 es6 中的模块。

为了支持CommonJS和AMD的exports, TypeScript提供了export =语法。 export = 语法定义一个模块的导出对象。 这里的对象一词指的是类,接口,命名空间,函数或枚举。

export = ZipCodeValidator;
编译成
module.exports = ZipCodeValidator;

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

import zip = require("./ZipCodeValidator");
编译成
var zip = require("./ZipCodeValidator");

命名空间

把所有与验证器相关的类型都放到一个叫做Validation的命名空间里。 因为我们想让这些接口和类在命名空间之外也是可访问的,所以需要使用 export。 相反的,变量 lettersRegexp和numberRegexp是实现的细节,不需要导出,因此它们在命名空间外是不能访问的。

namespace Validation {    //命名空间
    export interface StringValidator {     //此接口在该命名空间外可访问。
        isAcceptable(s: string): boolean;
    }

    const lettersRegexp = /^[A-Za-z]+$/;    
    //此变量并没有向外导出,因此不能在命名空间外使用。
    const numberRegexp = /^[0-9]+$/;

    export class LettersOnlyValidator implements StringValidator {
        isAcceptable(s: string) {
            return lettersRegexp.test(s);
        }
    }
}

装饰器

装饰器(Decorators)是一种特殊类型的声明,它可以被附加到类声明、方法、属性或参数上。装饰器有@符号紧接一个函数名称,如:@expression,expression装饰器函数必须返回是一个函数,这个函数会在该装饰器被使用的时候调用(而不是new的时候,new的时候早就装饰完成了)。装饰器是用来给附着的主题进行装饰,添加额外的行为。(装饰器属于ES7规范)。

启用装饰器模式:tsconfig.json中compilerConfig配置experimentalDecorators

在需要被装饰的类或方法前通过 @装饰器名称 来调用装饰器,会对下面紧挨着的类进行装饰;

类装饰器

        @f
	class 类名 { xxx }
	//装饰器可以累加,可以一行也可以多行书写

类装饰器应用于构造函数,可以用来监视、修改或替换类定义,类的构造函数会作为类装饰器函数的唯一一个参数类构造器相当于类构造函数的工厂函数。 代码上理解,类装饰函数相当于下面代码

function f (constructor: {new ( )}) { xxx }

装饰函数修改构造函数

function f<T extends {  new(...args: any[]):{}  }>(constructor: T) {
    return class extends constructor {
        age: number = 35;
    }
}

类装饰器,需要动态传参,装饰函数的工厂函数,这个返回的函数才时实际的装饰器函数

function f(arg: Object) {
    return function<T extends {  new(...args: any[]):{}  }>(constructor: T) {
        return class extends constructor {
            age: number = arg.age;
        }
    }
}

调用

@f({age: 35})
class 类名 { xxx }

方法装饰器

用来监视、修改或者替换方法定义

方法装饰器会在调用时传入下列3个参数:
对于静态成员来说是类的构造函数,对于实例成员来说是类的原型对象target
成员的名称name
成员属性描述符descriptor

function readOnly(target,name,descriptor){
    Descriptor.writable = false;
    Descriptor.calue = xxxx;
    Return descriptor
}

JSX

TypeScript支持内嵌,类型检查以及将JSX直接编译为JavaScript.

想要使用JSX必须做两件事:

  • 给文件一个.tsx扩展名
  • 启用jsx选项 TypeScript具有三种JSX模式:preserve,react和react-native。 这些模式只在代码生成阶段起作用 - 类型检查并不受影响。 image.png

TypeScript在.tsx文件里禁用了使用尖括号的类型断言。

类型检查

为了理解JSX的类型检查,你必须首先理解固有元素基于值的元素之间的区别。

假设有这样一个JSX表达式,
expr可能引用环境自带的某些东西(比如,在DOM环境里的div或span)或者是你自定义的组件。 这是非常重要的,原因有如下两点:
(1)对于React,固有元素会生成字符串(React.createElement("div")),然而由你自定义的组件却不会生成(React.createElement(MyComponent))。
(2)传入JSX元素里的属性类型的查找方式不同。 固有元素属性本身就支持,然而自定义的组件会自己去指定它们具有哪个属性。

固有元素

固有元素使用特殊的接口JSX.IntrinsicElements来查找。 默认地,如果这个接口没有指定,会全部通过,不对固有元素进行类型检查。
然而,如果这个接口存在,那么固有元素的名字需要在JSX.IntrinsicElements接口的属性里查找

declare namespace JSX {
    interface IntrinsicElements {
        foo: any
    }
}
<foo />; // 正确
<bar />; // 错误

基于值的元素

基于值的元素会简单的在它所在的作用域里按标识符查找

import MyComponent from "./myComponent";
<MyComponent />; // 正确
<SomeOtherComponent />; // 错误

有两种方式可以定义基于值的元素:

  • 无状态函数组件 (SFC)
  • 类组件 由于这两种基于值的元素在JSX表达式里无法区分,因此TypeScript首先会尝试将表达式做为无状态函数组件进行解析。如果解析成功,那么TypeScript就完成了表达式到其声明的解析操作。如果按照无状态函数组件解析失败,那么TypeScript会继续尝试以类组件的形式进行解析。如果依旧失败,那么将输出一个错误。