TypeScript

环境搭建

  • npm i typescript -g
    • 只需要安装一次
  • tsc --init
    • 会生成tsconfig.json文件
  • 打开配置文件中"outDir": "./",的注释
    • 根据需求修改
  • tsc 文件名
    • 执行编译生成js文件
    • tsc --version 获取版本号

VS半自动化环境

  • 终端
  • 运行任务
  • tsc 监视...
    • 即可实现变化保存自动编译为js文件

也可以配置npm脚本实现

"build": "tsc",
"build:watch": "tsc --watch"

编译指令

tsc 1.ts --outDir ./dist
如果不加--outDir则默认会编译到ts同一目录
ts-node

各种规范(和ts无关)

  • AMD CMD require.js sea.js 都基本上过时,不需要关心
  • node commonjs commonjs2
  • es6 module
  • umd 兼容以上三种

tsconfig


"./src":只编译src目录下文件,内部子文件夹不编译

"./src//":代表递归编译文件夹内部所有子文件夹 后面代表所有文件

ts-node:其他类型编译器 ts-node直接编译


直接运行

  • vscode按照coderunner插件
  • npm install ts-node -g
  • 运行即可

ts数据类型


数字,字符串,布尔值

null underfined
它们俩是其他类型的子类型,可以赋值给其他类型
例如:let name:string=null;

但是需要打开:"strictNullChecks": false, 否则报错

不然就只能let name:string|null=null;

数组 元组 枚举

void any Never


类型系统


string number boolean 基本类型

String Number Boolean 对象类型


基本类型可以赋值给包装类型,但是反之不可

数组(必须存储同一类型)

//基本语法
//数组的声明,此时push是没用的,因为未定义
// let arr:number[];
//数组的定义
let arr:number[]=[];
//泛型方式
// let arr1:Array<number>;
arr.push(...[1,2,4])
console.log(arr)

元组(类型不必相同)

长度和类型都确定的数组,而且后面数据和前面类型必须一一对应

let data:[number,string,boolean];
data=[1,"a",true];
说明:元组再3.1之后,不能越界使用联合类型了,而且赋值和定义的类型要一一对应

联合类型

//多个类型中的一个  或的关系
let a:string|number|boolean=20;
a="a";
console.log(a)

枚举

enum Color{
    RED,
    YELLOW
}
console.log(Color.RED);//0
console.log(Color.YELLOW);//1

enum Color{
    RED=1,
    YELLOW
}
console.log(Color.RED);//1
console.log(Color.YELLOW);//2


//可以修改某个值然后后续的值顺延即可
enum Week{
    MONDAY=2,
    TUESDAY
}
console.log(Week.TUESDAY);//3
  • 常数枚举

常数枚举后续不可修改,所以在编译成js时候直接输出部分是0,1数字而不是变量

const enum Colors{
    Red,
    Yellow
}
console.log(Colors.Red,Colors.Yellow); //0  1
  • 枚举的兼容性

枚举类型与数字类型兼容,并且数字类型与枚举类型兼容;不同枚举类型之间是不兼容的


Never

其他类型的子类型

说明:代表那些永远不存在的值的类型,ts也可以自动推断,never此时也可以省略

返回值是never的函数,永远不能正常结束,必须是类
似于抛出错误这种,也就代表着永远没有返回值,异常不会正常走返回
function err():never{
    throw new Error("error")
}

Any

说明:任意类型,在不确定数据类型的情况下使用
let a:any="a";
a=10;
console.log(a)

类型综合

/**
 * 数据类型:
 * 布尔类型(boolean)
 * 数字类型(number)
 * 字符串类型(string)
 * 元组类型(tuple)
 * 枚举类型(enum)
 * 任意类型(any)
 * null和underfined
 * void类型
 * never类型:从不会出现的值
 */

 //不赋值,也不会有默认值
let flag:boolean=true
//第一种定义数组的方式
let arrs:number[]=[1,2,3]
//第二种定义数组的方式
let arrs1:Array<number>=[1,2,3]
//第三种定义数组的方式
let arrs4:Array<any>=[1,'3',true] //不会报错

//元组类型:属于数组的一种,此时数据类型和后面赋值要一一对应
let arrs2:[number,string]=[123,'this is ts']

//枚举类型
enum WEEK{
    success=1, //指定枚举从1开始,不指定则默认0起始
    error,//此时不指定,则为2,如果多个值中间指定赋值,则后面的依此加一
    'underfined'=-1,
    'null'=-2
}
let w:WEEK=WEEK.error;
console.log(WEEK.underfined); //-1

//任意类型
let num:any=123;
num='asdas';

//任意类型使用场景
let item:any=document.getElementById('test');

//null和underfined是其他(never)数据类型的子类型
let num1:undefined;
// console.log(num1) //不报错,如果定义为number类型则报错了

let num2:undefined|number;
// console.log(num2);//兼具两者的优势


function run():void{

}
//声明never的变量只能被never类型赋值:代表从不会出现的值
let a:never;
a=(()=>{
    throw new Error('错误')
})()

函数

//函数表达式
let f:()=>string=function():string{
    return "a"
 }
  let f:()=>void=function(){
 }
//函数声明
function fn(x:number,y:number):number{
    return x+y
}

type

type用来定义类型或者类型别名

type GetUserName = (firstName: string, lastName: string) => {name:string};
let getUserName: GetUserName = function (firstName: string, lastName: string): {name:string} {
    return {name:firstName+lastName};
}
  • 泛型类型别名(扩展)
type Cart<T>={list:T[]}|T[]; //联合类型
let c1:Cart<string>={list:['1']};
let c2:Cart<string>=['1']

可选参数和参数默认值

说明:通过?来定义可选参数
    function fn(x:Type,y?:Type):Type
    可选参数默认为undefined
    可选参数必须在必传参数之后
//可选参数
function fn(x:number,y?:number):number{
   return x+y
}
//参数默认值,其实因为类型推导,可直接写成y=1
function fn1(x:number,y:number=1):number{
   return x+y
}
console.log(fn(1));//NaN
console.log(fn1(1));//2
补充:可选参数和默认值不要用在一个参数上

剩余参数

//剩余参数
function fn2(...arg:any[]){
   console.log(arg) //[ 1, 2, 3 ]
}
fn2(1,2,3)


function sum(...numbers:Array<number>) {
    //accu是最终要返回的值
    return numbers.reduce((accu,item)=>accu+item,0);
}

函数重载

//注意:这三行除了注释之外,必须紧紧的贴在一起,否则报错

//定义函数的重载格式
function fn(x:number,y:string);
function fn(x:number,y:number);
//定义函数具体的实现
function fn(x:any,y:any){
   console.log(x+y)
}
fn(1,2) //3
fn(1,"2")//12
  • 函数参数的协变

不推荐这么使用,了解即可

type logFunc=(a:number|string)=>void;
let log:logFunc;
function log1(a:number|string|boolean) {
    
}
log=log1;//正确
//其实很好理解,ts不认类型,只要包含即可,明显log1更多选择可以赋值给更小选择的

函数综合

//函数声明
function run1():void{

}
//匿名函数
let run2=function():number{
    return 123;
}

let fun3=function(name:string,age:number):string{
    return name+'---'+age;
}
// console.log(fun3('zq',12));
//可选参数:必须是再最后面,不能再前面
function fun4(name:string,age?:number):void{}
fun4('zq') 

//默认参数:可选参数可以再默认参数之前
function fun5(name?:string,age:number=20):void{}
fun5('zq') 

//剩余参数
function fun6(...res:number[]):number{
    let sum=0;
    for (let index = 0; index < res.length; index++) {
        sum+=res[index];
    }
    return sum;
}
// console.log(fun6(1,2,3,4,5,6));//21

//剩余参数形式二
function fun7(a:number,...res:number[]):number{
    let sum=0;
    for (let index = 0; index < res.length; index++) {
        sum+=res[index];
    }
    return sum;
}
// console.log(fun7(1,2,3,4,5,6));//20

//es5中出现同名函数,下面会替换上面的,即使参数不同

//函数重载:必须要有一个any的实现
function func(name:string):string;
function func(age:number):number;
function func(str:any):any{
    if(typeof str==='string'){
        return '我叫: '+str
    }else{
        return '我的年龄是: '+str
    }
}

// console.log(func('a'),func(12));我叫: a 我的年龄是: 12

ts中的this

案例一:
let obj={
   a:10,
   fn(){
       //函数中默认this指向是any,通过下面再配置文件中解决,而且如果是类似于
       //document中事件的this,ts会自动推导出类型,this指向不需要下面配置也是事件对象
      //  "noImplicitThis": true
      console.log(this.a)
        //注意:此时this的指向只是是否有提示的问题,真的执行代码配置文件设置不设置值都是10
   }
}
案例二:
let obj={
   a:20,
   fn(this:Document){
      console.log(this.querySelector)
   }
}
document.onclick=obj.fn
说明:如果配置配置了this指向"noImplicitThis": true,
则fn中的this指向就是obj对象,但是此时obj.fn指向了
点击事件,为了有提示信息,需要手动指定this指向this:Document
这个this参数其实是一个假参数,ts编译时候会被去掉,纯粹是
为了代码提示而存在,this指向配置不配置,修改不修改都不影响最后的结果

修饰符

public protected(该类和子类能访问) private(类,对象内部) readonly(类,对象内部可用,其他只读)

案例一:
class Person{
   readonly n:number;
   constructor(num:number){
      this.n=num;
   }
}
let p=new Person(20)
案例二:简写方式
class Person{
   constructor(public num:number){
      this.n=num;
   }
}
let p=new Person(20)
说明:因为ts不同于js,构造函数中属性需要先声明才能
使用,此时public num:number此种方式就是相当于提前再
class中先声明了一份

存取器

class Person{
   //私有属性,大家默认的规则是下划线
   private _num:number;
   //存取器
   //存取器再ts中不是当做方法使用的,而是被当做属性
   get num():number{
      return this._num
   }
   set num(num:number){
      if(num>0){
         this._num=num;
      }
   }
}
let p=new Person()
p.num=-10
console.log(p.num);//undefined

静态

class Person {
   private static instance;
   private constructor() { }
   public static getInstance() {
      if (!Person.instance) {
         Person.instance = new Person();
      }
      return Person.instance;
   }
}
let p=Person.getInstance();//相等
let p1=Person.getInstance();

//注意:静态属性和静态方法都可被子类继承

抽象类

抽象描述一种抽象的概念,无法被实例化,只能被继承;无法创建抽象类的实例,抽象方法不能在抽象类中实现,只能在抽象类的具体子类中实现,而且必须实现。

abstract class Person {
   constructor() { }
   abstract study():void;
}
class Student extends Person{
   study(): void {
      console.log("学习");
   }
}
let s=new Student();
s.study()

重载和重写

  • 重写是指子类重写继承父类中的方法
  • 重载是指为同一个函数提供多个类型定义
class Animal {
    speak(word:string){
        console.log('a');
    }
}
class Cat {
    //重写
    speak(word:string){
        console.log('b');
    }
}

重载其实就是函数重载

类综合

class Person{
    private name:string;
    constructor(n:string){
        this.name=n;
    }
    run():void{
        console.log(this.name);
    }

}
/**
 * 类中属性修饰符
 * public: 公有 都可以访问,默认值
 * protected: 保护类型  在该类和子类能访问
 * private: 在该类能访问
 */
class Student extends Person{
    //实际上在新版本ts,构造函数在此时可省略,方法执行依然正常
    // constructor(n:string){
    //     super(n)
    // }
    static sex='男';//静态属性

    //静态方法里面只能使用静态属性
    static print(){
        console.log("静态方法",this.sex);
    }

    //重写父类方法
    run():void{
        console.log("重写方法");
    }
}
let s=new Student('zq');
// s.run() //zq ,不重写父类方法的情况下
// Student.print()

// s.run() //输出: 重写方法       重写父类的方法

/**
 * 抽象类:
 *      提供其他类继承的基类,不能实例化
 * abstract关键字定义抽象类和抽象方法,抽象类中的抽象方法不包含具体实现并且必须在派生类中实现
 * 
 * 抽象类中可以有属性,构造函数,和非抽象方法
 */

 abstract class Animal {
     name:string;
     constructor(name:string){
        this.name=name;
     }
     abstract eat():any;
     run(){
         console.log(this.name+"   跑步");
     }
 }

 class Dog extends Animal{
     eat() {
         console.log("狗吃饭");
     }
     
 }

 let d=new Dog('pf');
 d.eat();
 d.run();//pf   跑步

接口

  1. 用来描述对象的属性以及类型
interface Point{
    x:number;
    y:number;
}
let ponit:Point={x:0,y:0};
  1. 描述行为的抽象
//接口不能有任何属性和方法实现,只能有抽象描述
interface Options{
   num:number;
   //可选的
   name?:string;
   say();
}
class optImpl implements Options{
   num: number;
   constructor(num:number){
      this.num=num;
   }
   say() {
      console.log(this.num+"说话");
   }
}
function fn(opts:Options) {
   opts.say()
}
fn(new optImpl(20))
  1. 类可以实现多个接口,但是只能继承一个父类

接口补充

  • 接口的readonly
interface Circle{
    readonly PI:number;
}
let circle:Circle={
    PI:3.14
}
// circle.PI=0; 无法分配到 "PI" ,因为它是只读属性
  • 索引签名(任意属性)
/**
 * 索引签名:
 *    希望规则是:一组由数字进行key命名的对象
 * 补充:索引签名的key类型只能是string或者number
 * 索引签名在下面传参时候,是可有可没有的,而且不限制个数
 */
interface Options{
   //key是number,value是any类型的数据
   [atrr:number]:any;
}

function fn(opts:Options) {
}
fn({
   0:1,
   2:20
})
namespace a{
    interface PlainObject{
        [propNmae:string]:number;//key  value形式;这样可以任意多个值
    }
    let obj:PlainObject={
        x:1,
        y:2,
        z:3
    }
    
    
//这数组进行约束,因为数组的key其实就是索引,所以本质还是key-value形式
    interface UserInterface{
        [index:number]:string;
    }
    let arr:UserInterface=['1','2','3']
}
  • 接口约束构造函数

使用new来约束构造函数

interface WithNameClass{
    new(name:string):Animal;
}
class Animal{
    //public name:string 就相当于 this.name=name了
    constructor(public name:string){}
}

//使用
function createAnimal(clazz:WithNameClass,name:string) {
    return new clazz(name);
}
createAnimal(Animal,'zq');

断言

interface Options{
   num:number;
   name:string;
}

function fn(opts:Options) {
}
//断言
//按理说必须传入{num:20,name:"呵呵"}类似的才能通过
//但是通过断言可强制判定传入参数是什么类型
fn({} as Options)

补充:
let obj={
   num:10,
   name:"saa",
   a:1
}
fn(obj)
说明:如果把传入的参数先赋值好在传入,可以避免规则检测
不会报错,但是此种情况只能在传入的obj覆盖全部所需参数
情况下,也就是说只能多不能少

接口的兼容性

ts跟类型没有关系,只和有没有有关系;简单说:我包含你的,我就可以给你这种类型传参

interface Animal{
    name:string;
}
interface Person{
    name:string;
    speak:(words:string)=>void;
}

function getNmae(animal:Animal) {
    console.log(animal.name);
}
let p:Person={
    name:'zq',
    speak(){

    }
}
getNmae(p);
//此处传p也正确,因为Animal有的Person都有,所以也符合规则

函数类型接口

/**
 * 函数类型接口
 * 是一个包含由fn并且值的类型为函数的结构体
 * 并不是描述函数结构而是一个包含函数的对象结构
 */
interface Options{
   fn:Function
}
let o:Options={
   fn:function(){

   }
}

/**
 * 下面约定就是函数结构,而不是包含有函数的对象结构了
 */
interface IFn{
   (x:number):number
}
let fn:IFn=function(x:number){return x}


/**
 * 下面是函数结构的实践
 *    因为interface的约定,保证了传参的正确性
 *    在编译阶段避免了出错
 */
interface MouseEventCallBack{
   (e:MouseEvent):any;
}
let fn1:MouseEventCallBack=function(e:MouseEvent){}
document.onclick=fn1;

补充案例

interface AjaxData{
   code:number;
   data:any;
}
interface AjaxCallBack{
   (rs:AjaxData):any
}
function ajax(callback:AjaxCallBack){
   callback({
      code:200,
      data:{}
   })
}

泛型

泛型是指在定义函数、接口或类的时候,不预先指定具体的类型,而是在使用的时候再指定类型的一种特性;泛型T作用域只限于函数内部使用

/**
 * 泛型:
 *    很多时候,类型写死,不利于复用
 */
//泛型变量
function fn<T>(args:T):T{
   return args;
}
function fn1<T,S>(args:T,args1:S):[T,S]{
   return [args,args1];
}

//数组形式
function fn2<T>(args:T[]):T[]{
   return args;
}
function fn3<T>(args:Array<T>){}

泛型类

class MyArray<T>{
   private _data:T[]=[];
   public push(v:T):number{
      return this._data.length;
   }
}
let a=new MyArray<string>();
a.push("a")

let b=new MyArray<number>();
b.push(1)

泛型类型

//泛型类型
let fn:<T>(x:T,y:T)=>number=function(x,y){
   return Number(x)+Number(y)
}
let fn1=function<T,S>(x:T,y:S):number{
   return Number(x)+Number(y)
}
console.log(fn(1,2));//3
console.log(fn1<number,string>(1,"2"));//3

泛型接口

interface IFN<T,S>{
   (x:T,y:S):S
}
let fn2:IFN<string,number>=function(x,y){
   return Number(x)+Number(y);
}

接口泛型

很奇怪的用法,不太常用

interface Calculate{
    <T>(a:T,b:T):T
}
let add:Calculate=function<T>(a:T,b:T):T{
<!--报错,因为传入类型不一定可以相加-->
    <!--return a+b; -->
    return a;
}
add<number>(1,2);
  • (泛型可以有多个)面试题:不增加中间变量的情况下,交换两个变量的值
function swap<A,B>(tuple:[A,B]):[B,A] {
    return [tuple[1],tuple[0]];
}
swap<string,number>(['zq',10]);

默认泛型类型

function swap<A=number,B=string>(tuple:[A,B]):[B,A] {
    return [tuple[1],tuple[0]];
}
swap([1,'zq']);
// swap<string,number>(['zq',1]);

类类型

/**
 * 类类型:
 *    表示这个类型对应的对象
 */

 //错误实例:此时Array代表就是类类型,但是需要的参数是该类
 //对应的构造函数
function getArray1(constructor:Array<number>){
   return new constructor();
}
getArray2(Array)

//补充:p后面的Person就是类类型
let p:Person=new Person();
//下面是构造函数
let fn1:{new ():Person}


//正确写法
function getArray2(constructor:{new():Array<string>}){
   return new constructor();
}
getArray2(Array)

泛型约束

形式一:
function fn<T extends number>(a:T){
   console.log(a);
}

形式二:
interface Len{
   length:number
}
function fn1<T extends Len>(a:T){
   console.log(a.length);
}
fn1("a")//此时在fn1(1)则会报错,因为数字类型没有length属性

接口综合

//ts自定义方法传入参数  对json进行约束
function printLabel(labelInfo:{label:string}):void{
    console.log("printlabel");
}

// printLabel('name')//错误
// printLabel({name:'haha'})//错误
printLabel({label:'haha'}) //正确

//一、函数类型接口
/**
 * 以上只是针对单一方法进行约束,那么批量约束呢
 * 1. 属性接口   对json的约束
 */
interface FullName{
    firstName:string;
    secondName:string; 
}

function printName(name:FullName){
    console.log(name.firstName,name.secondName);
}
let obj={
    age:20,
    firstName:'zq',
    secondName:'pf',
}
//注意: 传入对象引用,只要包含必须项即可,但是如果直接传入对象,则不能包含多余属性
//但是建议,不要添加多余属性
printName(obj)


/**
 * 2. 可选属性
 */
interface FullName1{
    firstName:string;
    secondName?:string; //可选属性
}

function printName1(name:FullName1){
    console.log(name.firstName,name.secondName);
}
printName1({firstName:'zq'})//zq undefined

/**
 * 3.函数类型接口
 * 对方法的传入参数和返回值进行约束
 */

 interface encrypt{
     (key:string,value:string):string;
 }
 let md5:encrypt=function(key:string,value:string):string{
    return 'haha';
 }
//1. 可索引接口:数组,对象的约束(不常用)
interface UserArr{
   [index:number]:string;
}
let arr:UserArr=['aaa','bbb']


interface UserObj{
   [index:string]:string;
}
let obj1:UserObj={name:'20'}

//2. 类类型接口: 对类的约束和抽象类有点相似
interface Animal1{
   name:string;
   eat1(str:string):void;
}
class Dog1 implements Animal1{
   name: string;    
   constructor(name:string){
       this.name=name;
   }
   eat1(str: string): void {
       console.log(this.name,str);
   }
}
let d1=new Dog1('zq');
d1.eat1('haha'); //zq haha
//1. 接口扩展:接口继承其他接口
interface A1{
    eat():void;
}
interface A2{
    fly():void;
}
//接口可以多继承
interface P1 extends A1,A2{
    run():void
}
//类只能单继承,但是可以多实现,而且继承和实现可并存
class P2 implements P1{
    run(): void {

    }    
    eat(): void {

    }
    fly(): void {

    }
}

泛型综合

// 泛型基本使用
function getData<T>(name:T):T{
    return name;
}
console.log(getData('zq'));
console.log(getData(12));

//泛型类
class MinClass<T>{
    public list:T[]=[]
    add(value:T):void{
        this.list.push(value)
    }
    min():T{
        let min=this.list[0];
        return min;
    }
}
let m=new MinClass<number>();

// 泛型接口
//1. 方式一
interface ConfinFn{
    <T>(v1:T):T;
}
let fn:ConfinFn=function<T>(v1:T):T{
    return v1;
}
fn<string>('haha')

//2. 方式二
interface ConfinFn1<T>{
    (v1:T):T;
}
function fn2<T>(v1:T):T{
    return v1;
}
let fn3:ConfinFn1<string>= fn2;
fn3('sad')

/**
 *泛类 把类当作参数的泛型类
 */

class User{
    //之所以加underfined是因为不初始化报错,再多加一个类型就不报错了
    username:string|undefined; 
    password:string|undefined;
}

//这样就可以把各种外部类传递进来
class MysqlDB<T>{
    add(user:T):boolean{
        return true;
    }
}
let m1=new MysqlDB<User>();
// m1.add(new User())

命名空间和模块

export.ts

export namespace B {
    export let url = 'safas';
    export function getData(): any[] {
        return [
            1, 2, 3
        ]
    }
    // export {url,getData} 一次统一暴露,但是使用此方式,上面的export就不能存在了

    // export default getData 一个模块只能用一次,引入方式也有区别
}

export namespace C{
    export let url1 = 'safas';
    export function getData1(): any[] {
        return [
            1, 2, 3
        ]
    }
}
modules.ts

/**
 * 命令空间: 内部模块,主要用于组指代码,避免命名冲突(其实针对的就是一个文件同名方法的冲突解决方案)
 * 模  块 : ts的外部模块的简称,侧重代码复用,一个模块里可能有多个命令空间
 */
import { B,C } from './export';
// import getData from './export';   export default方式导出的时候的引入方式

//如果导出时候没有命名空间 则可以通过as取别名
import { getData as get } from './export';
B.getData();

装饰器

  • 方式一:配置文件(开启装饰器):"experimentalDecorators": true
  • 方式二:根据vscode错误提示(装饰器错误提示)也可以自动修改
/**
 * 装饰器:
 *  类装饰器   属性装饰器  方法装饰器   参数装饰器
 * 
 * 装饰器写法:
 *      普通装饰器(无法传参)
 *      装饰器工厂(可传参)
 * 
 * 通俗的讲:装饰器就是一个方法
 */

 //1. 类装饰器
namespace A{
    function logclass(params:any){
        //params其实就是被装饰的类
    //    console.log(params);
       //扩展属性
       params.prototype.apiURL='www.zq.com';
       params.prototype.run=function(){
           console.log('run');
       }
    }
   
    @logclass
    class HttpClient{
       getData(){
       }
    }
    let h:any=new HttpClient();
    // console.log(h.apiURL); //www.zq.com
//    h.run()
}



//2. 类装饰器(带参数)
namespace B{
    function logclass(params:string){
        //此时params就是hello
        //target就是被装饰的类
        return function(target:any){
            // console.log(params,target);
        }
    }
    
    //装饰的过程是直接执行的,不需要实例化,不需要调用
    @logclass('hello')
    class HttpClient{
       getData(){
       }
    }
    let h=new HttpClient();
}



/**
 * 重载构造的小例子
 * 类的装饰器表达式会在运行时当作函数被调用,类的构造函数作为其唯一的参数
 * 如果类的装饰器返回一个值,它会使用提供的构造函数来替换类的声明
 */
namespace C{
    function logclass(target:any){
        return class extends target{
            apiUrl='修改路径';
            getData(){
                console.log('修改前');
                this.apiUrl='修改路径'+'-----';
                console.log(this.apiUrl);
            }
        }
    }
    
    @logclass
    class HttpClient{
        apiUrl:string|undefined;
        constructor(){
            this.apiUrl='路径初始化'
        }
       getData(){
           console.log(this.apiUrl);
       }
    }
    let h=new HttpClient();
    // h.getData() //修改前  修改路径-----
}

/**
 * 属性装饰器:
 *  属性装饰器表达式会在运行时当作函数被调用,传入下列2个参数:
 *      1. 对于静态成员来说是类的构造函数(例如 :Person),对于实例成员是类的原型对象(例如:Person.prototype)
 *      2. 成员的名字
 */

namespace D{
    function logurl(params:string){
        return function(target:any,propertyName:string){
            // console.log(target,propertyName);
            //虽然这样可修改,但是通过打印发现target[propertyName]是undefined,很奇怪
            //而且必须这个属性如果在构造函数有赋值,构造函数内部赋值会替换装饰器的赋值,因为构造是后执行的
            target[propertyName]=params;//相当于用@logurl('xxx')的参数给apiUrl重新赋值
            
            //注意:后面还有Object.defineProperty的形式实现的,不直接这么 target[propertyName]=params赋值;因为有的是没有参数的,需要动态监听
        }
    }
    
    class HttpClient{
        @logurl('xxx')
        apiUrl:string|undefined;
       getData(){
           console.log(this.apiUrl);
       }
    }
    let h=new HttpClient();
    // h.getData()
}






/**
 * 方法装饰器:
 *      会被应用到方法的属性描述符上,可以用来监视,修改或者替换方法定义
 * 方法装饰器参数:
 *  1. 对于静态成员来说是类的构造函数,对于实例成员是类的原型对象:{getData: ƒ, constructor: ƒ}
 *  2. 成员的名字(方法名称): "getData" 
 *  3. 成员的属性描述符: {value: ƒ, writable: true, enumerable: true, configurable: true};
 * 重点: 另外属性描述器的value属性就是指向被装饰的方法
 */


 //方式装饰器一
namespace D{
    function logMethods(params:any){
        return function(target:any,methodsName:string,desc:PropertyDescriptor){
            // console.log(target,methodsName,desc);
    
            //扩展属性和方法
            target.apiUrl=params;
            target.run=function(){
                console.log(this.apiUrl);
            }
        }
    }
    
    class HttpClient{
        @logMethods('www')
        getData(){
            console.log('执行');
        }
    }
    let h:any=new HttpClient();
    // h.getData();//执行
    // h.run();//www
}

//方法装饰器二
namespace E{
    function logMethods(params:any){
        return function(target:any,methodsName:any,desc:PropertyDescriptor){
            /**
             * ƒ () {
                    console.log('执行');
                }
             */
            // console.log(desc.value);
    
            //修改装饰器的方法,把装饰器方法里面传入的参数修改为string类型
            
            //1. 保存当前方法
            let om=desc.value;
            desc.value=function(...args:any[]){
                console.log("执行原始方法前");
                args=args.map((value)=>{
                    return String(value)
                })
                //如果需要绑定上下文,甚至传参到原始方法
                om.apply(this,args);
                console.log("执行原始方法后");
            }
            //如果不涉及上下文调用原始的getData方法,则可以把方法调用放在外面
            // om();
        }
    }
    
    class HttpClient{
        @logMethods('www')
        getData(...args:any[]){
            args.forEach(element => {
                console.log(element);
            });
        }
    }
    let h:any=new HttpClient();
    // h.getData(1,2,3,4);
    /**
     * 输出结果:
     * 执行原始方法前
        1
        2
        3
        4
     执行原始方法后
     */
}

/**
 * 方法参数装饰器:用处不大,可能有复杂用法,但是基本可以通过类装饰器替代,不必研究
 *      参数装饰器表达式会在运行时当作函数被调用,可以使用参数装饰器为类的原型增加一些元素数据,
 *  参数列表:
 *  1. 对于静态成员来说是类的构造函数(Person),对于实例成员是类的原型对象:{getData: ƒ, constructor: ƒ}   例如:Person.prototype
 *  2. 方法的名字: "getData"
 *  3. 参数在函数参数列表中索引 : 0
 */

 //? 代表可选参数:可传可不传
function logParams(params?:any){
    return function(target:any,methodsName:string,index:number){
        // console.log(target,methodsName,index);
        target.apiURL=params;//扩展属性
    }
}

class HttpClient{
    getData(@logParams('xxx') uuid:any){
        console.log(uuid);
    }
}
let h:any=new HttpClient();
h.getData();


/*
* 1. 属性和方法装饰器,谁先写谁先执行
* 2. 方法装饰器又分为方法的和方法参数的,先参数后方法
* 3. 最后是类装饰器
* 4. 同类型的,先执行后写的,从内到外
*/
  • 属性装饰器和方法装饰器
namespace a1{
    function upperCase(target:any,prototyName:string) {
        let value=target[prototyName];
        const getter=()=>value;
        const setter=(newVal:string)=>{
            value=newVal.toUpperCase();
        }
        delete target[prototyName];//删掉之前的属性描述器
        Object.defineProperty(target,prototyName,{
            get:getter,
            set:setter,
            enumerable:true,
            configurable:true
        })
    }

    //方法的
    function methodEnumerable(params:boolean) {
        //propertyDescriptor:老的属性描述器
        return function(target:any,prototyName:string,propertyDescriptor:PropertyDescriptor) {
            propertyDescriptor.enumerable=params;
        }
    }
    class Person {
        @upperCase
        name:string='zq';

        @methodEnumerable(true)
        public getName(){

        }
    }
    let p=new Person();
    console.log(p.name);//ZQ
    for (const attr in p) {
        console.log(attr);
    }
}

类型保护

  • 类型保护就是一些表达式,他们再编译的时候就能通过类型信息确保某个作用域内变量的类型
  • 类型保护就是能通过关键字判断出分支中的类型

以下是案例:

  • typeof
//最主要就是再对应判断里面有对应函数的提示
function double(input: string | number | boolean) {
    if (typeof input==='string') {
        input.toLowerCase();
    }else  if (typeof input==='number') {
        input.toFixed(2);
    }else{
        input;
    }
}
  • instanceof

重点是加了判断,代码提示也有了,而且不会提示多余的属性

class Animal {
   public name:string='zq';
}
class Bird extends Animal{
    public swing:number=2;
}
function getName(a:Animal) {
    if (a instanceof Bird) {
        a.swing;
    } else {
        a.name;
    }
}
  • 链式判断运算符
//先判断a是不是null/underfined,如果是则直接返回null或underfined,否则返回a.b的值
a?.b
  • 可辨识的联合类型

利用联合类型中的共有字段进行类型保护的一种技巧,相同字段的不同取值就是可辨识

interface WarningButton{
    class:'warning',
    text1:'1'
}
interface DangerButton{
    class:'danger',
    text2:'11'
}

type Button=WarningButton|DangerButton;
function getButton(btn:Button) {
    //直接写,代码只提示共有属性class

    //如下才行
    if (btn.class==='warning') {
        btn.text1;
    }else{
        btn.text2;
    }
}
  • in操作符
interface WarningButton{
    text1:'1'
}
interface DangerButton{
    text2:'11'
}

function getButton(btn:WarningButton|DangerButton) {
    if ('text1' in btn) {
        btn.text1;
    } else {
        btn.text2;
    }
}
  • 自定义的类型保护

自定义类型保护,其实就是定义了一个函数,函数的返回值是一个类型的谓词,形式是params is Type;params必须是当前函数签名里面的一个参数名

interface WarningButton{
    name1:'1',
    text1:'1'
}
interface DangerButton{
    name2:'2',
    text1:'11'
}

function isWarning(x:WarningButton|DangerButton):x is WarningButton {
    //规则自定义
    return x.text1==='1';
}

function getButton(btn:WarningButton|DangerButton) {
    if (isWarning(btn)) {
        btn.name1;
    } else {
        btn.name2;
    }
}

类型变换

  • 交叉类型
//多个类型的叠加,并且的关系
let b:string&number=


interface Bird{
    name:string;
    fly():void;
}
interface Person{
    name:string;
    eat():void;
}
//交叉类型其实就是两个接口类型的属性的并集
type BirdMan=Bird&Person;
  • typeof

可以获取一个变量的类型

/*
type Person={
    name:string;
    age:number
}
*/

let p={
    name:'zq',
    age:10
}
type Person=typeof p;
let p2:Person={
    name:'qq',
    age:20
}
  • 索引访问操作符

通过[]获取一个类型的子类型

interface Person{
    name:string;
    job:{
        name:string
    }
}
let n:Person['job']['name']='fe';
  • keyof

索引类型查询操作符

interface Person {
    name: string;
    gender: 'male' | 'female';
    //方式一:添加任意属性
    // [propName:string]:any;
}


//报错:因为无法确定传入的key,可能Person里面没有这个key,所以return报错
// function getValueKey(val: Person, key: string): any {
//     return val[key];
// }

//方式二
// type PKeys='name'|'gender';
type PKeys=keyof Person;  //返回一个接口key的集合;这样避免修改key之后也要修改此处
function getValueKey(val: Person, key: PKeys): any {
    return val[key];
}

let p: Person = {
    name: 'zq',
    gender: 'female'
};
getValueKey(p,'gender');
  • 映射类型

再定义的时候用in操作符批量定义

interface Person {
    name: string;
    gender: 'male' | 'female';
}

type PartialPerson={
    //? 代表可选 变成赋值的时候不需要全部都写了
    [key in keyof Person]?:Person[key]
}
let p: PartialPerson = {
    name: 'zq',
    // gender:'female'   可以不写,因为成了可选的
};

内置工具类型

TS中内置了一些工具类型来帮助我们更好的使用类型系统,虽然下面案例都是使用接口的,但是实际上class也可以的

  • Partial

将传入的属性由非可选变成可选

interface Person {
    name: string;
    gender: 'male' | 'female';
}
let p: Partial<Person> = {
    name: 'zq'
};

实现原理

type Partial<T>={
    [key in keyof T]?:T[key]
}
  • Required

将传入的属性中的可选项变为必选项

interface Person {
    name: string;
    gender?: 'male' | 'female';
}
let p: Required<Person> = {
    name: 'zq',
    gender:'female'
};

实现原理

type Required<T>={
    //注意这里是-?
    [key in keyof T]-?:T[key]
}
  • ReadOnley

属性变成只读

interface Person {
    name: string;
    gender?: 'male' | 'female';
}
type ReadOnlyPerson=Readonly<Person>
let p: ReadOnlyPerson = {
    name: 'zq',
    gender:'female'
};
// p.gender='zq';无法分配到 "gender" ,因为它是只读属性。

实现原理

type Readonly<T>={
  Readonly  [key in keyof T]:T[key]
}
  • Pick

从传入的属性中摘取某一项返回

interface Person {
    name: string;
    gender?: 'male' | 'female';
}
type PickPerson=Pick<Person,'name'>;
let p: PickPerson = {
    name: 'zq'
};

实现原理

//keyof T=name|gender 是个联合类型
type Pick<T,K extends keyof T>={
    [key in K]:T[key]
}
  • 映射类型修饰符的控制

TS中增加了对映射类型修饰符的控制,具体而言,一个readonly或?修饰符里可以用前缀+或-来表示这个修饰符应该被添加或移除。TS中部分内置工具类型利用了这个特性(Partial...)

条件类型

再定义泛型的时候能够添加进逻辑分支,泛型使用更灵活

案例一

interface A{
    name1:string
}
interface B{
    name2:string
}
interface C{
    name3:string
}
interface D{
    name4:string
}

//判断T是不继承A
//ts判断是否继承是根据属性,所以即使传进入是A,因为属性都有,所以也可以
type Condition<T>=T extends A?B:C;
let c:Condition<A>={name2:'zq'};

案例二

interface A{
    name1:string
}
interface B{
    name2:string
}
interface C{
    name3:string
}
interface D{
    name4:string
}

type Condition<T>=T extends A?B:C;
//其实最终效果就是 B和C的交集
let c:Condition<A|D>={name2:'zq',name3:'zq1'};

TS内置了很多条件类型

  • Exclude

从T可分配的类型中排除U

type E=Exclude<string|number,string>;
let e:E=10;
  • Extract

从T可分配的类型中提取U

type E=Extract<string|number,string>;
let e:E='1';
  • NonNullable

从T中排除null和underfined

type E=NonNullable<string|number|null|underfined>;
let e:E='hello';
let e:E=10;
  • ReturnType

获取函数类型的返回类型,并作为新的类型

function getUserInfo() {
    return {name:'zq',age:10}
}
type UserInfo=ReturnType<typeof getUserInfo>
let u:UserInfo={name:'zq',age:20};
  • InstanceType

获取构造函数的实例类型

class Person {
    name:string;
    constructor(name:string) {
        this.name=name;
    }
}
type P=InstanceType<typeof Person>
let p:P={name:'zq'};
let p:P=new Person('zq');

类型声明

  • 声明文件可以让我们不需要将js重构为TS,只需要加上声明文件就可以使用系统
  • 类型声明再编译的时候都会被删除,不会影响真正的代码
  • 可以把类型声明放在一个单独的类型声明文件中
  • 文件命令规范为*.d.ts
  • @types是一个约定前缀,所有第三方声明的类型库都会有这样的前缀(例如@types/node @types/jquery)
  • TS核心库的类型声明文件
  • 一般*.d.ts文件都在typings文件夹下面

使用案例

//jquery.d.ts
declare function jQuery(selector: string): any;

declare namespace jQuery {
    function ajax(url: string): void;
}
//需要导出,否则外部无法使用
export default jQuery;

声明示例

declare let age: number;
declare function getName(): string;
declare class Animal {
    naem: string
}
declare interface Person {
    name: string
}
//declare内部不需要加declare
declare namespace jQuery{
    function ajax(url:string):void;
    let name:string;
    namespace fn{
        function extend(object:any):void;
    }
}

注意:vscode下一般都可以找到,但是我们需要是的特定目录下找到定义文件,所以可以通过tsconfig.json文件实现。

补充

  1. 无法重新声明块范围变量“name”

在ts中有个lib.dom.ts;类似于全局文件,里面声明的有name属性,所以这样直接写不行,需要把该ts文件转换成模块

  1. 代码里面有export import之类的代码,那么这个文件就变成一个模块
  2. ts为dom提供了一整套类型声明
let root: HTMLElement | null = document.getElementById('root');
//!强行断言 root不是null,这样就不会报错了,否则root可能为null,说白了欺骗编译器
root!.style.color = 'red';
  1. 包装对象 (java中的装箱拆箱)
let name1: string = 'zf';
// name1.toLowerCase();
let name11 = new String(name1);
name11.toLowerCase();

let isOk1: boolean = true;
let isOk2: boolean = Boolean(1);  //拆箱为boolean
// let isOk3:boolean=new Boolean(1);  报错  对象赋值给了基本类型

自动在基本类型和对象类型之间切换;基本类型上没有方法;在内部迅速的完成一个装箱的操作,把基本类型迅速包装成对象类型,然后用对象来调用方法

  1. 类型断言
let name2: string | number;

此时直接调用name2只能调用string和number的共同方法;要么调用前赋值,然后回自动推导;要么就使用类型断言,就可以正常调用方法了;(name2 as string).toLowerCase();

  1. 字面量类型

后面赋值只能是'boy'或'girl'
; let G1:'boy'|'girl';

  1. 使用注意
关键字 作为类型使用 作为值使用
class yes yes
enum yes yes
interface yes no
type yes no
function no yes
var,let,const no yes

例如:interface接口就不能作为值赋值,一般使用最多的就是定义类型

最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 203,324评论 5 476
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 85,303评论 2 381
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 150,192评论 0 337
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 54,555评论 1 273
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 63,569评论 5 365
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 48,566评论 1 281
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 37,927评论 3 395
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 36,583评论 0 257
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 40,827评论 1 297
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 35,590评论 2 320
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 37,669评论 1 329
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 33,365评论 4 318
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 38,941评论 3 307
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 29,928评论 0 19
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 31,159评论 1 259
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 42,880评论 2 349
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 42,399评论 2 342

推荐阅读更多精彩内容