深入浅出学习TypeScript

lxf2023-03-18 09:58:01

什么是TypeScript

ts本质上Javascript的一个超集,众所周知js是一个弱类型语言,在开发编写时存在着很多不规范不严谨的现象,而ts在原有的语法基础上,添加了可选的静态类型和基于类的面向对象编程,使得我们可以在编写变量时直接添加其变量类型,随后在编译期间会进行类型检查,如果发现变量值的类型和默认定义类型不一致,则抛出错误提示进行纠正。
故在项目中引入ts会使我们的代码更加严谨,尤其是在团队庞大项目中使用会明显比js减少多人协作风格不一的影响和代码维护难度。所以许多大型公司的大型项目对ts十分青睐有加,今天就让我们一起来学习一下ts丰富我们的知识面,但是由于一些基础语法官方文档给比较详细,查看基础语法可以直接去官网查询,本文会结合笔者实际项目中对ts的使用来列举一些ts中略微复杂的知识点。

TypeScript的特性

首先明确一点,ts不是一门新语言,ts文件最终还是会编译成js文件进行执行,所以js还是前端的交互的基石,这一点不容置疑。所以ts和js一样都是弱语言类型,只不过不同的是js的类型检测是在运行中进行的,而且可以直接在浏览器中运行,而ts的类型检测是在编译时就可以直接检测进行报错,不能再浏览器中直接运行需要先通过编译器编译成js才能运行。

TypeScriptTS基础类型与写法

基础类型

boolean、string、number、array、null、undefined,为ts里面的基础类型基本上是和js一致

 // js写法
    let isBoolean = true;
    let name = 'zhangsan';
    let age = 26;
    let undef = undefined;
    let n = null;
    let stringArr = ['aaa', 'bbb'];
    // ts写法
    let isBoolean: boolean = true;
    let name: string = 'zhangsan';
    let age: number = 26;
    let undef: undefined = undefined;
    let n: null = null;
    //两种形式都是可以表示预设类型为数组里面值类型为string
    let stringArr: string[] = ['aaa', 'bbb'];
    let stringArr: Array<string> = ['aaa', 'bbb'];

元组 Tuple

元组是ts中提出的一个新类型,他可以允许你表示一个已知元素数量和类型的数组,写法如下

let newTupleArray:[string,number];
newTupleArray=['1',2]
//报错
newTupleArray=[1,'2']
//注意,元组刚开始定义的类型顺序必须要保持一致,对应位置对应类型不同就会报错,但是超长之后就是这之前类型中的一种即可,如果数据不是在之前定义类型中也会报错
//正常
newTupleArray=['1',2,4,5,'232']
//报错 newTupleArray[2]为boolean不属于string或number
newTupleArray=['1',2,true,5,'232']

枚举 enum

枚举类型是ts对js语言的一种补充,像C#等等的传统编程语言一样,他可以对数据进行更方便的赋值。

enum stateType={
success,//0
padding,//1
error//2
}  

let success: stateType = Score.success;

enum的赋值默认是从0开始的,但是支持自定义赋值

//注意我此处定义的都为string类型
  enum stateType={
    success = '1',//1
      padding = '2',//2
      error = '3'//3
  }

  let success: stateType = Score.success;


也支持混合赋值,而且需要特别注意的是,enum的默认赋值是以上一个number类型的值为基础进行递增(此处面试中很常见)。

  enum stateType={
    success,//0
      padding = 'padding',//padding
      error//1
  }

  enum colorType={
    red,//0
      yellow = 5,//number类型 5
      error//6
  }

enum还有一个非常好用的特性是反向映射,可以通过键进行获取值,也可以通过值获取键

    // 反向映射
    enum enumObject {
        A,
        B,
        C,
        D
    }
    console.log(Score.A)//0
    console.log(Score[1])//B

enum面试扩展

    // 面试题:指出异构的枚举值
    // 异构
    enum Enum {
        A,      // 0
        B,      // 1
        C = 'C',
        D = 'D',
        E = 6,
        F,      // 7
    }
    // 面试题: 手写将其转化为JS实现
    let Enum;
    (function(Enum) {
        // 正向
        Enum['A'] = 0;
        Enum['B'] = 1;
        Enum['C'] = 'C';
        Enum['D'] = 'D';
        Enum['E'] = 6;
        Enum['F'] = 7;

        // 反向
        Enum[0] = 'A';
        Enum[1] = 'B';
        Enum[6] = 'E';
        Enum[7] = 'F';
    })(Enum || (Enum = {}));
```

any 暂不清楚的类型

当你在ts中将一个变量类型定义为any,ts将不会在在进行对此变量进行类型检测,因为官方解释是此类型代表是你暂时不知道此变量类型为什么,或者此变量的值是动态获取的,故提供此类型避免编译阶段报错。但是这个类型在实际开发中很容易就被滥用,一时any一时爽,一直any一直爽。。。(但是any滥用会导致问题排查困难,你根本不知道当前变量是什么类型,慎用小心被同事打)

//看名字是number类型实际上鬼知道他是什么类型
let numberA:any={
a:'1'
}
//需要注意any是绕过了所有的类型检查所以any类型可以被赋值给其他非any类型

let a:string='类型检查'
let b:any=0
a=b//b虽然不是string类型但是可以被赋值给a,因为any会绕过所有的类型检查

unknown 未知类型

unknown虽然作用和any相似但是权限没有any高,unknown只是绕过了自身的赋值类型检查,但是其他变量的类型检查依旧生效。

let a:unknown='类型检查'
let b:unknown=0
let c:boolean=true
a=b//不会报错,因为两者都是unknown类型
c=b//报错,因为b不是布尔类型

Void 函数无返回

void类型和any相似但是特指函数无返回

function setNumber(nunber:Number):void{
let a=number

}

Never 不存在的值的类型

never类型表示的是那些永不存在的值的类型。 never类型是那些总是会抛出异常或根本就不会有返回值的函数表达式或箭头函数表达式的返回值类型;变量也可能是 never类型,当它们被永不为真的类型保护所约束时。

never类型是任何类型的子类型,也可以赋值给任何类型;然而,没有类型是never的子类型或可以赋值给never类型(除了never本身之外)。 即使 any也不可以赋值给never。

// 返回never的函数必须存在无法达到的终点 
function error(message: string): never { throw new Error(message); }

Object

请注意这个Object和原生Object不同,ts在自身里面进行扩展。

   // TS将js的object分成两个接口来定义
    interface ObjectConstructor {
        create(o: object | null): any;
    }
    interface Object {
        constructor: Function;//对创建此对象的数组函数的引用
        toString(): string;//返回一个表示该对象的字符串
        toLocaleString(): string;//语言转换
        valueof(): Object;
        hasOwnProperty(v: PropertyKey):boolean;//判断属性是否是在此对象上
        isPrototypeOf(v: Object): boolean;//判断属性是否是在此对象的原型链
    }
    
    //
    create({ prop: 0 }); // OK 
    create(null); // OK 
    create(42); // Error           
    create("string"); // Error 
    create(false); // Error 
    create(undefined); // Error

需要特别注意的一点空对象{}

let a={}
a.a='xxx'//因为初始化未定义a所以ts编译器不知道a的类型所以a赋值失败
//但是空对象可以直接调用object方法
a.toString()//正常使用

interface 接口

接口是ts的核心原则之一是对值所具有的结构进行类型检查,方便我们进行自定义的属性定义

interface infClass={
a:string,
b:number,
c:boolean,
}
let infObject:infClass={
a:'1',
b:2,
c:false,
}

可选属性

interface infClass={
a?:string,
b?:number,
c:boolean,
}
//a,b可以不存在但是存在必须是对应的类型
let infObject:infClass={
c:false,
}

只读属性

interface infClass={
 readonly a:string,
b:number,
c:boolean,
}
let infObject:infClass={
a:'1',
b:2,
c:false,
}
infObject.a=2//不可更改直接报错

//面试题
constreadonly的区别
const是在运行阶段进行检测,同时const如果定义的是复杂数据类型,可以更改地址里面的值但是不能改变引用的地址  
readonly实在编译阶段进行检测,他编译时如果发现对只读属性值的任何更改操作都会直接报错。

& 合并

这是一个抽象的概念操作符,他的主要作用是将多个独立的声明进行合并,合并之后的新声明包含着所有独立声明的特性。不过需要留意如果合并时两个声明都有相同的元素但是类型不同会导致此属性被标记为never。

//多个声明进行合并
 interface A {x: D}
    interface B {x: E}
    interface C {x: F}

    interface D {d: boolean}
    interface E {e: string}
    interface F {f: number}

    type ABC = A & B & C;

    let abc: ABC = {
        x: {
            d: false,
            e: 'zhaowa',
            f: 5
        }
    }
 //合并的冲突
  interface A {
        c: string,
        d: string
    }
    interface B {
        c: number,
        e: string
    }

    type AB = A & B;
    let ab: AB = {
        d: 'class',
        e: 'class'
    }
    //此时c被置为never类型,因为不会存在一个变量既是number又是string
    //如果想解决此冲突只在一个声明中进行定义c即可

断言

由于ts实在编译时进行检测,如果我们知道某个变量的是某种类型可以通过断言来与编译器进行交流,告诉编译器此变量是什么类型。

 // 尖括号形式声明
    let anyValue: any = 'hello word';
    let anyLength: number = (<string>anyValue).length;

    // as声明
    let anyValue: any = 'hello word';
    let anyLength: number = (anyValue as string).length;
    
    
     // 肯定化保证
    let score: number;
    startClass();
    console.log(5 * score);

    function startClass() {
        score = 5;
    }
    let score!: number; // 告知编辑器,运行时会被赋值的

类型守卫

这个和我们前端里面常用的路由守卫相似,但是他是主要保障语法规定的范围内,额外的确认,保证类型的无误,也可以根据类型的不同进行不同的逻辑操作。

 interface Teacher {
        name: string;
        courses: string[];
        score: number;
    }
    interface Student {
        name: string;
        startTime: Date;
        score: string;
    }

    type Class = Teacher | Student;

    // in - 是否包含某种属性
    function startCourse(cls: Class) {
    //判断参数是那种类型
        if ('courses' in cls) {
            // 处理老师类型逻辑
        }
        if ('startTime' in cls) {
            // 处理学生类型逻辑
        }
    }
    
    // typeof / instanceof - 类型分类场景下的身份确认
    function startCourse(cls: Class) {
        if (typeof cls.score === 'number') {
           // 处理老师类型逻辑
        }
        if (typeof cls.score === 'string') {
             // 处理学生类型逻辑
        }
    }

    function startCourse(cls: Class) {
        if (cls instanceof Teacher) {
           // 处理学生类型逻辑
        }
        if (cls instanceof Student) {
             // 处理学生类型逻辑
        }
    }
    // 自定义类型
    const isTeacher = function(cls: Teacher | Student): cls is Teacher {
        // 通过返回值判断是否是老师还是学生
    }

    const getInfo = (cls: Teacher | Student) => {
        if (isTeacher(cls)) {
        //如果是老师就返回老师的信息
            return cls.courses;
        }
    }

ts进阶操作

函数重载

这是一个抽象的概念,主要是为了解决在js中调用函数是根据传入参数的类型不同来进行不同的处理,我们可以在代码中定义多个同名方法,每个方法提供多个传入参数类型定义来进行函数重载。 编译器会根据这个列表去处理函数的调用。如下所示

       //重点关注的是传入参数不同
      function  start(name: number, score: number): number;
      function  start(name: string, score: string): string;
      function start(name: string, score: number): number;
      function start(name: Comnbinable, score: Comnbinable) {
      //通过类型守卫进行不同的处理
      //同时如果传入参数类型不在重载的函数列表中,ts会编译报错
            if (typeof name === 'number' || typeof score === 'number') {
                // 处理
            }
            if (typeof name === 'string' || typeof score === 'string') {
                // 处理
            }
            if (typeof name === 'string' || typeof score === 'number') {
                // 处理
            }
        }
 

泛型

这也是一个抽象的概念主要用途是提高函数复用性,如果你了解过c或者c#这类型语言你就会发现泛型在编程语言中很常见,正是因为泛型的存在它们可以编写出很多可重用的组件,一个组件可以支持多种类型数据进行处理。

//这里T和U都是代表任意类型,不具体指向某种类型
//注意,自由度越高后期维护成度越大,所以泛型和any一样慎用,用的太多会被人打的
 function startClass <T, U>(name: T, score: U): T {
 //此函数入参name要和返回参数类型一直,不然会报错
        // 逻辑
    }
    function startClass <T, U>(name: T, score: U): string {
        // 逻辑
    }
    function startClass <T, U>(name: T, score: T): T {
    //断言
        return (name + score) as T;
    }

修饰器

这个也是抽象类型在其他高级语言中很常见,是为了更加方便的为class、函数、属性来提供额外的功能,支持标注或修改类及其成员。

//若要启用实验性的装饰器特性,你必须在命令行或`tsconfig.json`里启用`experimentalDecorators`编译器选项:
tsc --target ES5 --experimentalDecorators

实际使用

//定义一个校长装饰器
  function principal(target: Function): void {
  //给老师类额外增加监督老师的功能
        target.prototype.supervise = function(): void {
            // 逻辑
        }
    }
//相当于把整个tercher当做参数传递进了装饰器principal
    @principal
    class tercher {
        constructor() {
            // 业务逻辑
        }
    }

    // 属性/方法装饰器
    function isPrincipal(target: any, key: string): void {
    //对传进来的参数进行数据劫持
        Object.defineProperty(target, key, {})
    }

    class tercher {
        constructor() {
            // 业务逻辑
        }

        @isPrincipal
        //劫持
        public name: string;
    }

ts编译原理

首先我们在ts文件中书写代码之后,ts会通过内部扫描器(scanner)生成令牌流,随后解析器(parser)会讲接收到的令牌流进行解析,解析成ast语法树,再通过绑定器(binder)将语法树和辅助校验器进行绑定,然后通过校验器(checker)检查之前生成的ast语法树,若检测出有类型异常则抛出异常,若检测通过则通过发射器(emitter)进行翻译,讲ts文件翻译成js文件进行运行。

 // 1. 源码
    var a = 2;

    // 2. scanner扫描器生成令牌流
    [
        'var': 'keyword',//操作命令key
        'a': 'identifier',//变量名
        '=': 'assignment',//赋值操作
        '2': 'imteger',//值
        ';': 'eos'//结束
    ]

    // 3. parser 解析器
    {
        operation: '=',
        left: {
            keyword: 'var',
            right: 'a'
        }
        right: '2'
    }

    // 4. binder绑定器
    // AST节点 node.symbol <=> 辅助校验器

    // 5.1. 校验器checker
    // ts节点语法检查 => 类型检查

    // 5.2 发射器emitter
    // 翻译完成每个node节点的内容,翻译成js => 输出

总结

Ts个人感觉是非常值得去掌握的一个知识点,因为严谨的类型检测对于大型项目的工作协调真的是非常有用,不仅如此现在大公司基本上都是在用ts进行项目开发,方便我们后期走上更高的舞台打下基础。同时也方便踏出走向全栈的路子,因为比这公司的node后台全部是使用的ts进行开发,笔者也写了一段时间见vue3+ts使用体验也是非常的棒。所以ts值得我们掌握,希望笔者的文章能帮助到大家,一起共勉。