TS的指南

目录

TS 是什么 ?

TS和JS的关系

安装TS

查看版本

编译

在线编译

TS的基本数据类型

基本类型

引用类型

Array

扫描二维码关注公众号,回复: 15686513 查看本文章

Tuple(元组)

object

function

函数重载

特殊类型

any

unknow

数字枚举

类型推论

void

never

Enum(枚举)

字符串枚举

常量枚举

异构枚举

字面量类型

交叉类型(&)

同名基础属性合并

同名非基础属性合并

Class(类)

基本方法

私有字段(#)

只读属性(readonly)

继承(extends)

修饰符

abstract

重写和重载

tsconfig.json

TS断言和类型守卫

TS断言

类型断言

非空断言

确定赋值断言

双重断言

类型守卫

in关键字

typeof关键字

interfaceof关键字

类型谓词(is)

两者的区别

类型别名、接口

类型别名(type)

接口(interface)

对象的形状

继承

函数类型接口

type 和 interface 的区别

基础数据类型

扩展

重复定义

联合类型(Union Types)

可辨识联合

泛型

为什么需要泛型

泛型语法

多类型传参

泛型接口

泛型类

泛型类型别名

泛型默认参数

泛型常用字母

常用技巧

extends

typeof

keyof

索引访问操作符

in

infer

Partial

Required

Readonly

Record

Pick

Exclude

Extra

Omit

NonNullable

ReturnType

Parameters

End

参考:

小结


TS 是什么 ?

TS:是TypeScript的简称,是一种由微软开发的自由和开源的编程语言。

你还可以看看:你所不了解的TypeScript 类型编程

目录

TS 是什么 ?

TS和JS的关系

安装TS

查看版本

编译

在线编译

TS的基本数据类型

基本类型

引用类型

Array

Tuple(元组)

object

function

函数重载

特殊类型

any

unknow

数字枚举

类型推论

void

never

Enum(枚举)

字符串枚举

常量枚举

异构枚举

字面量类型

交叉类型(&)

同名基础属性合并

同名非基础属性合并

Class(类)

基本方法

私有字段(#)

只读属性(readonly)

继承(extends)

修饰符

abstract

重写和重载

TS断言和类型守卫

TS断言

类型断言

非空断言

确定赋值断言

双重断言

类型守卫

in关键字

typeof关键字

interfaceof关键字

类型谓词(is)

两者的区别

类型别名、接口

类型别名(type)

接口(interface)

对象的形状

继承

函数类型接口

type 和 interface 的区别

基础数据类型

扩展

重复定义

联合类型(Union Types)

可辨识联合

泛型

为什么需要泛型

泛型语法

多类型传参

泛型接口

泛型类

泛型类型别名

泛型默认参数

泛型常用字母

常用技巧

extends

typeof

keyof

索引访问操作符

in

infer

Partial

Required

Readonly

Record

Pick

Exclude

Extra

Omit

NonNullable

ReturnType

Parameters

End

参考:

小结


TS和JS的关系

对比与JS,TS是JS的超集,简单的说就是在 JavaScript 的基础上加入了类型系统,让每个参数都有明确的意义,从而带来了更加智能的提示。

相对于JS而言,TS属于强类型语言,所以对于项目而言,会使代码更加规范,从而解决了大型项目代码的复杂性,其次,浏览器是不识别TS的,所以在编译的时候,TS文件会先编译为JS文件。

安装TS

执行命令:

$ npm install -g typescript 
 //或
$ yarn global add typescript

查看版本

$ tsc -v

编译

$ tsc test.ts

# test.ts => test.js

在线编译

我们为了方便起见,可以使用线上的编辑器:TypeScript Playground[1],像这样

a97795fc0f27a2121ca9bf83aa9a54e3.png

 image.png

并且你还可以看看生成对应的ts转化ES5ES6之后的代码,也有相关的例子供你查看

TS的基本数据类型

这里将TS的数据类型简单的进行下归类:

  • 基本类型:stringnumberbooleansymbolbigintnullundefined

  • 引用类型:array、 Tuple(元组)、 object(包含Object{})、function

  • 特殊类型:anyunknowvoidnerverEnum(枚举)

  • 其他类型:类型推理字面量类型交叉类型

注:案例中有可能用到typeinterface,在下面会详细讲解,有比较模糊的可以先看看

基本类型

//字符串
    let str: string = "Domesy"
    
    // 数字
    let num: number = 7
    
    //布尔
    let bool: boolean = true
    
    //symbol
    let sym: symbol = Symbol();
     
    //bigint
    let big: bigint = 10n
        
    //null
    let nu: null = null
    
    //undefined
    let un: undefined = undefined

需要注意:

  • null 和 undefined 两个类型一旦赋值上,就不能在赋值给任何其他类型

  • symbol是独一无二的,假设在定义一个 sym1,那么sym === sym1 为 false

引用类型

Array

两种方式:

  • 类型名称 + []

  • Array<数据类型>

let arr1: number[] = [1, 2, 3]
    
let arr2: Array<number> = [1, 2, 3]
    
let arr2: Array<number> = [1, 2, '3'] // error
    
 //要想是数字类型或字符串类型,需要使用 |
let arr3: Array<number | string> = [1, 2, '3'] //ok

Tuple(元组)

uple 可以说是 Array 的一种特殊情况,针对上面的 arr3,我们看他的类型可以是string也可以是number,但对每个元素没有作出具体的限制。

那么 Tuple 的作用就是限制元素的类型并且限制个数的数组,同时 Tuple这个概念值存在于TS,在JS上是不存在的

这里存在一个问题:在TS中,是允许对 Tuple 扩增的(也就是允许使用 push方法),但在访问上不允许

let t: [number, string] = [1, '2'] // ok
    let t1: [number, string] = [1, 3] // error
    let t2: [number, string] = [1] // error
    let t3: [number, string] = [1, '1', true] // error
 
 
    let t5: [number, string] = [1, '2'] // ok
    t.push(2)
    console.log(t) // [1, '2', 2]
 
    let a =  t[0] // ok
    let b = t[1] // ok
    let c = t[2] // error

object

  • object 非原始类型,在定义上直接使用 object 是可以的,但你要更改对象的属性就会报错,原因是并没有使对象的内部具体的属性做限制,所以需要使用 {} 来定义内部类型

let obj1: object = { a: 1, b: 2}
    obj1.a = 3 // error
 
    let obj2: { a: number, b: number } = {a: 1, b: 2}
    obj2.a = 3 // ok

 Object(大写的O),代表所有的原始类型或非原始类型都可以进行赋值,除了null和`undefined

let obj: Object;
    obj = 1; // ok
    obj = "a"; // ok
    obj = true; // ok
    obj = {}; // ok
    obj = Symbol() //ok
    obj = 10n //ok
    obj = null; // error
    obj = undefined; // error

function

定义函数

  • 有两种方式,一种为 function, 另一种为箭头函数

  • 在书写的时候,也可以写入返回值的类型,如果写入,则必须要有对应类型的返回值,但通常情况下是省略,因为TS的类型推断功能够正确推断出返回值类型

function setName1(name: string) { //ok
      console.log("hello", name);
    }
    setName1("Domesy"); // "hello",  "Domesy"
 
    function setName2(name: string):string { //error
      console.log("hello", name);
    }
    setName2("Domesy");
 
    function setName3(name: string):string { //error
      console.log("hello", name);
      return 1
    }
    setName3("Domesy");
 
    function setName4(name: string): string { //ok
      console.log("hello", name);
      return name
    }
    setName4("Domesy"); // "hello",  "Domesy"
 
    //箭头函数与上述同理
    const setName5 = (name:string) => console.log("hello", name);
    setName5("Domesy") // "hello",  "Domesy"

参数类型

  • 可选参数:如果函数要配置可有可无的参数时,可以通过 ? 实现,切可选参数一定要在最后面

  • 默认参数:函数内可以自己设定其默认参数,用 = 实现

  • 剩余参数:仍可以使用扩展运算符 ...

// 可选参数
    const setInfo1 = (name: string, age?: number) => console.log(name, age)
    setInfo1('Domesy') //"Domesy",  undefined
    setInfo1('Domesy', 7) //"Domesy",  7
 
    // 默认参数
    const setInfo2 = (name: string, age: number = 11) => console.log(name, age)
    setInfo2('Domesy') //"Domesy",  11
    setInfo2('Domesy', 7) //"Domesy",  7
 
    // 剩余参数
    const allCount = (...numbers: number[]) => console.log(`数字总和为:${numbers.reduce((val, item) => (val += item), 0)}`)
    allCount(1, 2, 3) //"数字总和为:6"

函数重载

函数重载:是使用相同名称和不同参数数量或类型创建多个方法的一种能力。在 TypeScript 中,表现为给同一个函数提供多个函数类型定义。简单的说:可以在同一个函数下定义多种类型值,总后汇总到一块

let obj: any = {};
    function setInfo(val: string): void;
    function setInfo(val: number): void;
    function setInfo(val: boolean): void;
    function setInfo(val: string | number | boolean): void {
      if (typeof val === "string") {
        obj.name = val;
      } else {
        obj.age = val;
      }
    }
    setInfo("Domesy");
    setInfo(7);
    setInfo(true);
    console.log(obj); // { name: 'Domesy', age: 7 }

特殊类型

any

在 TS 中,任何类型都可以归于 any 类型,所以any类型也就成了所有类型的顶级类型,同时,如果不指定变量的类型,则默认为any类型, 当然不推荐使用该类型,因为这样丧失了TS的作用

let d:any; //等价于 let d 
    d = '1';
    d = 2;
    d = true;
    d = [1, 2, 3];
    d = {}

unknow

any一样,都可以作为所有类型的顶级类型,但 unknow更加严格,那么可以说除了any 之下的第二大类型,接下来对比下any,主要严格于一下两点:

同时我们可以看看翻译为ES5是何样子

数字枚举

类型推论

我们在学完这些基础类型,我们是不是每个类型都要去写字段是什么类型呢?其实不是,在TS中如果不设置类型,并且不进行赋值时,将会推论为any类型,如果进行赋值就会默认为类型

image.png
image.png
image.png
image.png
  • unknow会对值进行检测,而类型any不会做检测操作,说白了,any类型可以赋值给任何类型,但unknow只能赋值给unknow类型和any类型

  • unknow不允许定义的值有任何操作(如 方法,new等),但any可以

    let u:unknown;
        let a: any;
     
        u = '1'; //ok
        u = 2; //ok
        u = true; //ok
        u = [1, 2, 3]; //ok
        u = {}; //ok
     
        let value:any = u //ok
        let value1:any = a //ok
        let value2:unknown = u //ok
        let value3:unknown = a //ok
        let value4:string = u //error
        let value5:string = a //ok
        let value6:number = u //error
        let value7:number = a //ok
        let value8:boolean = u //error
        let value9:boolean = a //ok
     
        u.set() // error
        a.set() //ok
        u() // error
        a() //ok
        new u() // error
        new a() //ok

    void

    当一个函数,没有返回值时,TS会默认他的返回值为 void 类型

    const setInfo = ():void => {} // 等价于 const setInfo = () => {}
     
        const setInfo1 = ():void => { return '1' }  // error
        const setInfo2 = ():void => { return 2 } // error
        const setInfo3 = ():void => { return true } // error
        const setInfo4 = ():void => { return  } // ok
        const setInfo5 = ():void => { return undefined } //ok

    never

    表示一个函数永远不存在返回值,TS会认为类型为 never,那么与 void 相比, never应该是 void子集, 因为 void实际上的返回值为 undefined,而 never 连 undefined也不行

    符合never的情况有:当抛出异常的情况和无限死循环

    let error = ():never => { // 等价约 let error = () => {}
                throw new Error("error");
        };
     
        let error1 = ():never => {
            while(true){}
        }

    Enum(枚举)

    可以定义一些带名字的常量,这样可以清晰表达意图创建一组有区别的用例

    注意:

  • 枚举的类型只能是 string 或 number

  • 定义的名称不能为关键字

  • 枚组的类型默认为数字类型,默认从0开始以此累加,如果有设置默认值,则只会对下面的值产生影响

  • 同时支持反向映射(及从成员值到成员名的映射),但智能映射无默认值的情况,并且只能是默认值的前面

  • ​编辑

    字符串枚举

    字符串枚举要注意的是必须要有默认值,不支持反向映射

    8ef7935a87cb22cdeab84f1ef963fcaf.png

    常量枚举

    除了数字类型字符串类型之外,还有一种特殊的类型,那就是常量枚组,也就是通过const去定义enum,但这种类型不会编译成任何 JS,只会编译对应的值

    bb6d3e56a011f17438923a90c5516d21.png

    异构枚举

    包含了 数字类型 和 字符串类型 的混合,反向映射一样的道理

    7836aaa7fa320e9006561e2642543a60.png
let a; // 推断为any
    let str = '小杜杜'; // 推断为string
    let num = 13; // 推断为number
    let flag = false; // 推断为boolean
 
    str = true // error Type 'boolean' is not assignable to type 'string'.(2322)
    num = 'Domesy' // error
    flag = 7 // error

字面量类型

字面量类型:在TS中,我们可以指定参数的类型是什么,目前支持字符串数字布尔三种类型。比如说我定义了 str 的类型是 '小杜杜' 那么str的值只能是小杜杜

let str:'小杜杜' 
    let num: 1 | 2 | 3 = 1
    let flag:true
 
    str = '小杜杜' //ok
    str = 'Donmesy' // error
 
    num = 2 //ok
    num = 7 // error
 
    flag = true // ok
    flag = false // error

交叉类型(&)

交叉类型:将多个类型合并为一个类型,使用&符号连接,如:

type AProps = { a: string }
    type BProps = { b: number }
 
    type allProps = AProps & BProps
 
    const Info: allProps = {
        a: '小杜杜',
        b: 7
    }

同名基础属性合并

我们可以看到交叉类型是结合两个属性的属性值,那么我们现在有个问题,要是两个属性都有相同的属性值,那么此时总的类型会怎么样,先看看下面的案例:

type AProps = { a: string, c: number }
    type BProps = { b: number, c: string }
 
    type allProps = AProps & BProps
 
    const Info: allProps = {
        a: '小杜杜',
        b: 7,
        c:  1, // error (property) c: never
        c:  'Domesy', // error (property) c: never
    }

如果是相同的类型,合并后的类型也是此类型,那如果是不同的类型会如何:

我们在ApropsBProps中同时加入c属性,并且c属性的类型不同,一个是number类型,另一个是string类型

现在结合为 allProps 后呢? 是不是c属性是 number 或 string 类型都可以,还是其中的一种?

然而在实际中, c 传入数字类型字符串类型都不行,我么看到报错,现实的是 c的类型是 never

这是因为对应 c属性而言是 string & number,然而这种属性明显是不存在的,所以c的属性是never

同名非基础属性合并

interface A { a: number }
    interface B { b: string }
 
    interface C {
        x: A
    }
    interface D {
        x: B
    }
    type allProps = C & D
 
    const Info: allProps = {
      x: {
        a: 7,
        b: '小杜杜'
      }
    }
 
    console.log(Info) // { x: { "a": 7, "b": "小杜杜" }}

我们来看看案例,对于混入多个类型时,若存在相同的成员,且成员类型为非基本数据类型,那么是可以成功合。

如果 接口A 中的 也是 b,类型为number,就会跟同名基础属性合并一样

Class(类)

ES6中推出了一个叫 class(类) 的玩意,具体定义就不说了,相信用过React的小伙伴一定不陌生.

基本方法

在基本方法中有:静态属性静态方法成员属性成员方法构造器get set方法,接下来逐个看看:

需要注意的是:在成员属性中,如果不给默认值,并且不使用是会报错的,如果不想报错就给如 ! ,如:name4!:string

class Info {
      //静态属性
      static name1: string = 'Domesy'
 
      //成员属性,实际上是通过public上进行修饰,只是省略了
      nmae2:string = 'Hello' //ok 
      name3:string //error
      name4!:string //ok 不设置默认值的时候必须加入 !
 
      //构造方法
      constructor(_name:string){
        this.name4 = _name
      }
 
      //静态方法
      static getName = () => {
        return '我是静态方法'
      }
 
      //成员方法
      getName4 = () => {
        return `我是成员方法:${this.name4}`
      }
 
      //get 方法
      get name5(){
        return this.name4
      }
 
      //set 方法
      set name5(name5){
        this.name4 = name5
      }
    }
 
    const setName = new Info('你好')
    console.log(Info.name1) //  "Domesy" 
    console.log(Info.getName()) // "我是静态方法" 
    console.log(setName.getName4()) // "我是成员方法:你好"

让我们看看上述代码翻译成ES5是什么样:

"use strict";
    var Info = /** @class */ (function () {
        //构造方法
        function Info(_name) {
            var _this = this;
            //成员属性
            this.nmae2 = 'Hello'; //ok
            //成员方法
            this.getName4 = function () {
                return "\u6211\u662F\u6210\u5458\u65B9\u6CD5:".concat(_this.name4);
            };
            this.name4 = _name;
        }
        Object.defineProperty(Info.prototype, "name5", {
            //get 方法
            get: function () {
                return this.name4;
            },
            //set 方法
            set: function (name5) {
                this.name4 = name5;
            },
            enumerable: false,
            configurable: true
        });
        //静态属性
        Info.name1 = 'Domesy';
        //静态方法
        Info.getName = function () {
            return '我是静态方法';
        };
        return Info;
    }());
    var setName = new Info('你好');
    console.log(Info.name1); //  "Domesy" 
    console.log(Info.getName()); // "我是静态方法" 
    console.log(setName.getName4()); // "我是成员方法:你好"

私有字段(#)

在 TS 3.8版本便开始支持ECMACMAScript的私有字段。

需要注意的是私有字段与常规字段不同,主要的区别是:

  • 私有字段以 # 字符开头,也叫私有名称;

  • 每个私有字段名称都唯一地限定于其包含的类;

  • 不能在私有字段上使用 TypeScript 可访问性修饰符(如 public 或 private);

  • 私有字段不能在包含的类之外访问,甚至不能被检测到。

    class Info {
          #name: string; //私有字段
          getName: string;
     
          constructor(name: string) {
            this.#name = name;
            this.getName = name
          }
     
          setName() {
            return `我的名字是${this.#name}`
          }
        }
     
        let myName = new Info("Domesy");
     
     
        console.log(myName.setName()) // "我的名字是Domesy" 
        console.log(myName.getName) // ok "Domesy" 
        console.log(myName.#name) // error 
        // Property '#name' is not accessible outside class 'Info' 
        // because it has a private identifier.(18013)

    只读属性(readonly)

    只读属性:用 readonly修饰,只能在构造函数中初始化,并且在TS中,只允许将interfacetypeclass上的属性标识为readonly

  • readonly实际上只是在编译阶段进行代码检查

  • radonly修饰的词只能在 constructor阶段修改,其他时刻不允许修改

    class Info {
          public readonly name: string; // 只读属性
          name1:string
     
          constructor(name: string) {
            this.name = name;
            this.name1 = name;
          }
     
          setName(name:string) {
            this.name = name // error
            this.name1 = name; // ok
          }
        }

    继承(extends)

    继承:是个比较重要的点,指的是子可以继承父的思想,也就是说 子类 通过继承父类后,就拥有了父类的属性和方法,这点与HOC有点类似

    这里又个super字段,给不知道的小伙伴说说,其作用是调用父类上的属性和方法

    // 父类
        class Person {
          name: string
          age: number
     
          constructor(name: string, age:number){
            this.name = name
            this.age = age
          }
     
          getName(){
            console.log(`我的姓名是:${this.name}`)
            return this.name
          }
     
          setName(name: string){
            console.log(`设置姓名为:${name}`)
            this.name = name
          }
        }
     
        // 子类
        class Child extends Person {
          tel: number
          constructor(name: string, age: number, tel:number){
            super(name, age)
            this.tel = tel
          }
     
          getTel(){
            console.log(`电话号码是${this.tel}`)
            return this.tel
          }
        }
     
        let res = new Child("Domesy", 7 , 123456)
        console.log(res) // Child {."name": "Domesy", "age": 7, "no": 1 }
        console.log(res.age) // 7
        res.setName('小杜杜') // "设置姓名为:小杜杜" 
        res.getName() //   "我的姓名是:小杜杜"
        res.getTel() //  "电话号码是123456"

    修饰符

    主要有三种修饰符:

  • public:类中、子类内的任何地方、外部都能调用

  • protected:类中、子类内的任何地方都能调用,但外部不能调用

    • private:类中可以调用,子类内的任何地方、外部均不可调用

      class Person {
            public name: string
            protected age: number
            private tel: number
       
            constructor(name: string, age:number, tel: number){
              this.name = name
              this.age = age
              this.tel = tel
            }
          }
       
          class Child extends Person {
            constructor(name: string, age: number, tel: number) {
              super(name, age, tel);
            }
       
            getName(){
              console.log(`我的名字叫${this.name},年龄是${this.age}`) // ok name 和 age可以
              console.log(`电话是${this.tel}`) // error 报错 原因是 tel 拿不出来
            }
          }
       
       
          const res = new Child('Domesy', 7, 123456)
          console.log(res.name) // ok Domesy
          console.log(res.age) // error
          console.log(res.tel) // error

      abstract

      abstract: 用abstract关键字声明的类叫做抽象类,声明的方法叫做抽象方法

    • 抽象类:指不能被实例化,因为它里面包含一个或多个抽象方法。

    • 抽象方法:是指不包含具体实现的方法;

    • 注:抽象类是不能直接实例化,只能实例化实现了所有抽象方法的子类

abstract class Person {
      constructor(public name: string){}
 
      // 抽象方法
      abstract setAge(age: number) :void;
    }
 
    class Child extends Person {
      constructor(name: string) {
        super(name);
      }
 
      setAge(age: number): void {
        console.log(`我的名字是${this.name},年龄是${age}`);
      }
    }
 
    let res = new Person("小杜杜") //error
    let res1 = new Child("小杜杜");
 
    res1.setAge(7) // "我的名字是小杜杜,年龄是7"

重写和重载

  • 重写:子类重写继承自父类中的方法

  • 重载:指为同一个函数提供多个类型定义,与上述函数的重载类似

    // 重写
        class Person{
          setName(name: string){
            return `我的名字叫${name}`
          }
        }
     
        class Child extends Person{
          setName(name: string){
            return `你的名字叫${name}`
          }
        }
     
        const yourName = new Child()
        console.log(yourName.setName('小杜杜')) // "你的名字叫小杜杜" 
     
        // 重载
        class Person1{
          setNameAge(name: string):void;
          setNameAge(name: number):void;
          setNameAge(name:string | number){
            if(typeof name === 'string'){
              console.log(`我的名字是${name}`)
            }else{
              console.log(`我的年龄是${name}`)
            }
          };
        }
     
        const res = new Person1()
        res.setNameAge('小杜杜') // "我的名字是小杜杜" 
        res.setNameAge(7) // "我的年龄是7"

    tsconfig.json

    tsconfig.json 包含 TypeScript 编译的相关配置,通过更改编译配置项,我们可以让 TypeScript 编译出 ES6、ES5、node 的代码。

    重要字段

    files - 设置要编译的文件的名称;
    include - 设置需要进行编译的文件,支持路径模式匹配;
    exclude - 设置无需进行编译的文件,支持路径模式匹配;
    compilerOptions - 设置与编译流程相关的选项。
    

    compilerOptions 选项

    {
      "compilerOptions": {
      
        /* 基本选项 */
        "target": "es5",                       // 指定 ECMAScript 目标版本: 'ES3' (default), 'ES5', 'ES6'/'ES2015', 'ES2016', 'ES2017', or 'ESNEXT'
        "module": "commonjs",                  // 指定使用模块: 'commonjs', 'amd', 'system', 'umd' or 'es2015'
        "lib": [],                             // 指定要包含在编译中的库文件
        "allowJs": true,                       // 允许编译 javascript 文件
        "checkJs": true,                       // 报告 javascript 文件中的错误
        "jsx": "preserve",                     // 指定 jsx 代码的生成: 'preserve', 'react-native', or 'react'
        "declaration": true,                   // 生成相应的 '.d.ts' 文件
        "sourceMap": true,                     // 生成相应的 '.map' 文件
        "outFile": "./",                       // 将输出文件合并为一个文件
        "outDir": "./",                        // 指定输出目录
        "rootDir": "./",                       // 用来控制输出目录结构 --outDir.
        "removeComments": true,                // 删除编译后的所有的注释
        "noEmit": true,                        // 不生成输出文件
        "importHelpers": true,                 // 从 tslib 导入辅助工具函数
        "isolatedModules": true,               // 将每个文件做为单独的模块 (与 'ts.transpileModule' 类似).
    
        /* 严格的类型检查选项 */
        "strict": true,                        // 启用所有严格类型检查选项
        "noImplicitAny": true,                 // 在表达式和声明上有隐含的 any类型时报错
        "strictNullChecks": true,              // 启用严格的 null 检查
        "noImplicitThis": true,                // 当 this 表达式值为 any 类型的时候,生成一个错误
        "alwaysStrict": true,                  // 以严格模式检查每个模块,并在每个文件里加入 'use strict'
    
        /* 额外的检查 */
        "noUnusedLocals": true,                // 有未使用的变量时,抛出错误
        "noUnusedParameters": true,            // 有未使用的参数时,抛出错误
        "noImplicitReturns": true,             // 并不是所有函数里的代码都有返回值时,抛出错误
        "noFallthroughCasesInSwitch": true,    // 报告 switch 语句的 fallthrough 错误。(即,不允许 switch 的 case 语句贯穿)
    
        /* 模块解析选项 */
        "moduleResolution": "node",            // 选择模块解析策略: 'node' (Node.js) or 'classic' (TypeScript pre-1.6)
        "baseUrl": "./",                       // 用于解析非相对模块名称的基目录
        "paths": {},                           // 模块名到基于 baseUrl 的路径映射的列表
        "rootDirs": [],                        // 根文件夹列表,其组合内容表示项目运行时的结构内容
        "typeRoots": [],                       // 包含类型声明的文件列表
        "types": [],                           // 需要包含的类型声明文件名列表
        "allowSyntheticDefaultImports": true,  // 允许从没有设置默认导出的模块中默认导入。
    
        /* Source Map Options */
        "sourceRoot": "./",                    // 指定调试器应该找到 TypeScript 文件而不是源文件的位置
        "mapRoot": "./",                       // 指定调试器应该找到映射文件而不是生成文件的位置
        "inlineSourceMap": true,               // 生成单个 soucemaps 文件,而不是将 sourcemaps 生成不同的文件
        "inlineSources": true,                 // 将代码与 sourcemaps 生成到一个文件中,要求同时设置了 --inlineSourceMap 或 --sourceMap 属性
    
        /* 其他选项 */
        "experimentalDecorators": true,        // 启用装饰器
        "emitDecoratorMetadata": true          // 为装饰器提供元数据的支持
      }
    }
    

  • TS断言和类型守卫

    TS断言

    分为三种:类型断言非空断言确定赋值断言

    当断言失效后,可能使用到:双重断言

    类型断言

    在特定的环境中,我们会比TS知道这个值具体是什么类型,不需要TS去判断,简单的理解就是,类型断言会告诉编译器,你不用给我进行检查,相信我,他就是这个类型

    共有两种方式:

  • 尖括号

  • as:推荐

    //尖括号
       let num:any = '小杜杜'
       let res1: number = (<string>num).length; // React中会 error
     
       // as 语法
       let str: any = 'Domesy';
       let res: number = (str as string).length;

    但需要注意的是:尖括号语法在React中会报错,原因是与JSX语法会产生冲突,所以只能使用as语法

    非空断言

    在上下文中当类型检查器无法断定类型时,一个新的后缀表达式操作符 ! 可以用于断言操作对象是非 null 和非 undefined 类型。

    我们对比下ES5的代码

    e0bfa6ce09c47ee51a3c5f47b86c6bc7.png image.png

    我们可以看出来 !可以帮助我们过滤 null和 undefined类型,也就是说,编译器会默认我们只会传来string类型的数据,所以可以赋值为str1

    但变成ES5后 !会被移除,所以当传入 null 的时候,还是会打出 null

    确定赋值断言

    TS 2.7版本中引入了确定赋值断言,即允许在实例属性和变量声明后面放置一个 ! 号,以告诉TS该属性会被明确赋值。

    let num: number;
        let num1!: number;
     
        const setNumber = () => num = 7
        const setNumber1 = () => num1 = 7
     
        setNumber()
        setNumber1()
     
        console.log(num) // error 
        console.log(num1) // ok

    双重断言

    断言失效后,可能会用到,但一般情况下不会使用

    失效的情况:基础类型不能断言为接口

    interface Info{
          name: string;
          age: number;
        }
     
        const name = '小杜杜' as Info; // error, 原因是不能把 string 类型断言为 一个接口
        const name1 = '小杜杜' as any as Info; //ok

    类型守卫

    类型守卫:是可执行运行时检查的一种表达式,用于确保该类型在一定的范围内

    我个人的感觉是,类型守卫就是你可以设置多种类型,但我默认你是什么类型的意思

    目前,常有的类型守卫共有4种:in关键字typeof关键字interfaceof关键字类型谓词(is)

    in关键字

    用于判断这个属性是那个里面的

    interface Info {
          name: string
          age: number
        }
     
        interface Info1{
          name: string
          flage: true
        }
     
        const setInfo = (data: Info | Info1) => {
          if("age" in data){
            console.log(`我的名字是:${data.name},年龄是:${data.age}`)
          }
     
           if("flage" in data){
            console.log(`我的名字是:${data.name},性别是:${data.flage}`)
          }
        }
     
        setInfo({name: '小杜杜', age: 7}) // "我的名字是:小杜杜,年龄是:7" 
        setInfo({name: '小杜杜', flage: true}) // "我的名字是:小杜杜,性别是:true"

    typeof关键字

    用于判断基本类型,如string | number等

    const setInfo = (data: number | string | undefined) => {
          if(typeof data === "string"){
            console.log(`我的名字是:${data}`)
          }
     
          if(typeof data === "number"){
            console.log(`我的年龄是:${data}`)
          }
     
          if(typeof data === "undefined"){
            console.log(data)
          }
        }
     
        setInfo('小杜杜') // "我的名字是:小杜杜"  
        setInfo(7) // "我的年龄是:7" 
        setInfo(undefined) // undefined"

    interfaceof关键字

    用于判断一个实例是不是构造函数,或使用类的时候

    class Name {
          name: string = '小杜杜'
        }
     
        class Age extends Name{
          age: number = 7
        }
     
        const setInfo = (data: Name) => {
          if (data instanceof Age) {
            console.log(`我的年龄是${data.age}`);
          } else {
            console.log(`我的名字是${data.name}`);
          }
        } 
     
        setInfo(new Name()) // "我的名字是小杜杜"
        setInfo(new Age()) // "我的年龄是7"

    类型谓词(is)

    function isNumber(x: any): x is number { //默认传入的是number类型
      return typeof x === "number"; 
    }
     
    console.log(isNumber(7)) // true
    console.log(isNumber('7')) //false
    console.log(isNumber(true)) //false

    两者的区别

    通过上面的介绍,我们可以发现断言类型守卫的概念非常相似,都是确定参数的类型,但断言更加霸道,它是直接告诉编辑器,这个参数就是这个类型,而类型守卫更像确定这个参数具体是什么类型。(个人理解,有不对的地方欢迎指出~)

    类型别名、接口

    类型别名(type)

    类型别名:也就是type,用来给一个类型起个新名字

    type InfoProps = string | number
        
        const setInfo = (data: InfoProps) => {}

    接口(interface)

    接口:在面向对象语言中表示行为抽象,也可以用来描述对象的形状。

    使用interface关键字来定义接口

    对象的形状

    接口可以用来描述对象,主要可以包括以下数据:可读属性只读属性任意属性

  • 可读属性:当我们定义一个接口时,我们的属性可能不需要全都要,这是就需要 ? 来解决

  • 只读属性:用 readonly修饰的属性为只读属性,意思是指允许定义,不允许之后进行更改

  • 任意属性:这个属性极为重要,它是可以用作就算没有定义,也可以使用,比如 [data: string]: any。比如说我们对组件进行封装,而封装的那个组件并没有导出对应的类型,然而又想让他不报错,这时就可以使用任意属性

    interface Props {
            a: string;
            b: number;
            c: boolean;
            d?: number; // 可选属性
            readonly e: string; //只读属性
            [f: string]: any //任意属性
        }
        let res: Props = {
            a: '小杜杜',
            b: 7,
            c: true,
            e: 'Domesy',
            d: 1, // 有没有d都可以
            h: 2 // 任意属性,之前为定义过h
        }
     
        let res.e = 'hi' // error, 原因是可读属性不允许更改

    索引签名

  • 定义: 索引签名用于描述那些“通过索引得到”的类型
  • 格式: 如[props: string]:any
  • 应用场景: 解决参数问题
    export default {}
    
    interface IFullName {
      firstName: string
      lastName : string
      age?: number
      singName?: string
      [props: string]: any
    }
    
    // 注意点: 如果使用接口来限定了变量或者形参, 那么在给变量或者形参赋值的时候,多一个或者少一个都不行
    // 实际开发中往往会出现多或者少的情况,怎么解决?
    
    
    // 少一个或者少多个属性
    // 解决方案: 可选属性
    let goddass1:IFullName = {firstName: "邱", lastName: "淑贞"};
    let goddass2:IFullName = {firstName: "邱", lastName: "淑贞", age: 18};
    
    
    // 多一个或者多多个属性
    // 方案一:使用变量
    let info = {firstName: "邱", lastName: "淑贞", age: 18, singName: "赌王", dance: "芭蕾"};
    let goddass3:IFullName = info
    
    // 方案二: 使用类型断言
    let goddass4:IFullName = ({firstName: "邱", lastName: "淑贞", age: 18, singName: "赌王", dance: "芭蕾"}) as IFullName;
    
    
    
    
    // 索引签名?
    // 索引签名用于描述那些“通过索引得到”的类型
    // 注意点: 对象中的键,会被转化为字符串
    interface Ibeauty {
      [props: string]: string
    }
    
    let name:Ibeauty = {name1: "邱淑贞", name2: "李嘉欣", name3: "周慧敏"};
    
    
    interface Iage {
      [props: string]: number
    }
    
    let afe:Iage = {age1: 18, age2: 20};
    
    
    
    
    
    
    
    // 方案三: 索引签名
    let goddass5:IFullName = {firstName: "邱", lastName: "淑贞", age: 18, singName: "赌王", dance: "芭蕾"};
    

  • 继承

    继承:与类一样,接口也存在继承属性,也是使用extends字段

    interface nameProps {
            name: string
        }
     
        interface Props extends nameProps{
            age: number
        }
     
        const res: Props = {
            name: '小杜杜',
            age: 7
        }

    函数类型接口

    同时,可以定义函数和类,加new修饰的事,不加new的事函数

    interface Props {
            (data: number): number
        }
     
        const info: Props = (number:number) => number  //可定义函数
     
        // 定义函数
        class A {
            name:string
            constructor(name: string){
                this.name = name
            }
        }
     
        interface PropsClass{
            new (name: string): A
        }
     
        const info1 = (fun: PropsClass, name: string) => new fun(name)
     
        const res = info1(A, "小杜杜")
        console.log(res.name) // "小杜杜"

    type 和 interface 的区别

    通过上面的学习,我们发现类型别名接口非常相似,可以说在大多数情况下,typeinterface是等价的

    但在一些特定的场景差距还是比较大的,接下来逐个来看看

    基础数据类型

  • typeinterface都可以定义 对象 和 函数

  • type可以定义其他数据类型,如字符串、数字、元祖、联合类型等,而interface不行

    type A = string // 基本类型
     
        type B = string | number // 联合类型
     
        type C = [number, string] // 元祖
     
        const dom = document.createElement("div");  // dom元素
        type D = typeof dom

    扩展

    interface 可以扩展 typetype 也可以扩展为 interface,但两者实现扩展的方式不同。

  • interface 是通过 extends 来实现

  • type 是通过 & 来实现

    // interface 扩展 interface
        interface A {
            a: string
        }
        interface B extends  A {
            b: number
        }
        const obj:B = { a: `小杜杜`, b: 7 }
     
        // type 扩展 type
        type C = { a: string }
        type D = C & { b: number }
        const obj1:D = { a: `小杜杜`, b: 7 }
     
        // interface 扩展为 Type
        type E = { a: string }
        interface F extends E { b: number }
        const obj2:F = { a: `小杜杜`, b: 7 }
     
        // type 扩展为 interface
        interface G { a: string }
        type H = G & {b: number}
        const obj3:H = { a: `小杜杜`, b: 7 }

    重复定义

    interface 可以多次被定义,并且会进行合并,但type不行

    interface A {
            a: string
        }
        interface A {
            b: number
        }
        const obj:A = { a: `小杜杜`, b: 7 }
     
        type B = { a: string }
        type B = { b: number } // error

    联合类型(Union Types)

    联合类型(Union Types) : 表示取值可以为多种类型中的一种,未赋值时联合类型上只能访问两个类型共有的属性和方法,如:

    const setInfo = (name: string | number) => {}
     
        setInfo('小杜杜')
        setInfo(7)

    从上面看 setInfo接收一个name,而 name 可以接收 stringnumber类型,那么这个参数便是联合类型

    可辨识联合

    可辨识联合:包含三个特点,分别是可辨识联合类型类型守卫,

    这种类型的本质是:结合联合类型字面量类型的一种类型保护方法。

    如果一个类型是多个类型的联合类型,且多个类型含有一个公共属性,那么就可以利用这个公共属性,来创建不同的类型保护区块。

    也就是上面一起结合使用,这里写个小例子:

    interface A {
          type: 1,
          name: string
        }
     
        interface B {
          type: 2
          age: number
        }
     
        interface C {
          type: 3,
          sex: boolean
        }
     
        // const setInfo = (data: A | B | C) => {
        //   return data.type // ok 原因是 A 、B、C 都有 type属性
        //   return data.age // error,  原因是没有判断具体是哪个类型,不能确定是A,还是B,或者是C
        // }
     
        const setInfo1 = (data: A | B | C) => {
          if (data.type === 1 ) {
            console.log(`我的名字是${data.name}`);
          } else if (data.type === 2 ){
            console.log(`我的年龄是${data.age}`);
          } else if (data.type === 3 ){
            console.log(`我的性别是${data.sex}`);
          }
        }
     
        setInfo1({type: 1, name: '小杜杜'}) // "我的名字是小杜杜"
        setInfo1({type: 2, age: 7}) // "我的年龄是7" 
        setInfo1({type: 3, sex: true}) // "我的性别是true"

    定义了 ABC 三次接口,但这三个接口都包含type属性,那么type就是可辨识的属性,而其他属性只跟特性的接口相关。

    然后通过可辨识属性type,才能使用其相关的属性

    泛型

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

    也就是说,泛型是允许同一个函数接受不同类型参数的一种模版,与any相比,使用泛型来创建可服用的组件要更好,因为泛型会保留参数类型(PS:泛型是整个TS的重点,也是难点,请多多注意~)

    为什么需要泛型

    我们先看看一个例子:

    const calcArray = (data:any):any[] => {
            let list = []
            for(let i = 0; i < 3; i++){
                list.push(data)
            }
            return list
        }
     
        console.log(calcArray('d')) // ["d", "d", "d"]

    上述的例子我们发现,在calcArray中传任何类型的参数,返回的数组都是any类型

    由于我们不知道传入的数据是什么,所以返回的数据也为any的数组

    但我们现在想要的效果是:无论我们传什么类型,都能返回对应的类型,针对这种情况怎么办?所以此时泛型就登场了

    泛型语法

    我们先用泛型对上面的例子进行改造下,

    const calcArray = <T,>(data:T):T[] => {
            let list:T[] = []
            for(let i = 0; i < 3; i++){
                list.push(data)
            }
            return list
        }
     
        const res:string[] = calcArray<string>('d') // ok
        const res1:number[] = calcArray<number>(7) // ok
     
        type Props = {
            name: string,
            age: number
        }
        const res3: Props[] = calcArray<Props>({name: '小杜杜', age: 7}) //ok

    经过上面的案例,我们发现传入的字符串数字对象,都能返回对应的类型,从而达到我们的目的,接下来我们再看看泛型语法

    function identity <T>(value:T) : T {
            return value
        }

    多类型传参

    我们有多个未知的类型占位,我们可以定义任何的字母来表示不同的参数类型

    const calcArray = <T,U>(name:T, age:U): {name:T, age:U} => {
            const res: {name:T, age:U} = {name, age}
            return res
        }
     
        const res = calcArray<string, number>('小杜杜', 7)
        console.log(res) // {"name": "小杜杜", "age": 7}

    泛型接口

    定义接口的时候,我们也可以使用泛型

    interface A<T> {
            data: T
        }
     
        const Info: A<string> = {data: '1'}
        console.log(Info.data) // "1"

    泛型类

    同样泛型也可以定义类

    class clacArray<T>{
            private arr: T[] = [];
     
            add(value: T) {
                this.arr.push(value)
            }
            getValue(): T {
                let res = this.arr[0];
                console.log(this.arr)
                return res;
            }
        }
     
        const res = new clacArray()
     
        res.add(1)
        res.add(2)
        res.add(3)
     
        res.getValue() //[1, 2, 3] 
        console.log(res.getValue) // 1

    泛型类型别名

    type Info<T> = {
            name?: T
            age?: T
        }
     
        const res:Info<string> = { name: '小杜杜'}
        const res1:Info<number> = { age: 7}

    泛型默认参数

    所谓默认参数,是指定类型,如默认值一样,从实际值参数中也无法推断出类型时,这个默认类型就会起作用。

    const calcArray = <T = string,>(data:T):T[] => {
            let list:T[] = []
            for(let i = 0; i < 3; i++){
                list.push(data)
            }
            return list
        }

    泛型常用字母

    用常用的字母来表示一些变量的代表:

  • T:代表Type,定义泛型时通常用作第一个类型变量名称

  • K:代表Key,表示对象中的键类型

  • V:代表Value,表示对象中的值类型

  • E:代表Element,表示的元素类型

常用技巧

在 TS 中有许多关键字和工具类型,在使用上,需要注意泛型上的应用,有的时候结合起来可能就有一定的问题

在此特别需要注意 extendstypeofPartialRecordExcludeOmit这几个工具类型

extends

extends:检验是否拥有其属性 在这里,举个例子,我们知道字符串数组拥有length属性,但number没有这个属性。

const calcArray = <T,>(data:T): number => {
      return data.length // error 
    }

上述的 calcArray的作用只是获取data的数量,但此时在TS中会报错,这是因为TS不确定传来的属性是否具备length这个属性,毕竟每个属性都不可能完全相同

那么这时该怎么解决呢?

我们已经确定,要拿到传过来数据的 length,也就是说传过来的属性必须具备length这个属性,如果没有,则不让他调用这个方法。

换句话说,calcArray需要具备检验属性的功能,对于上述例子就是检验是否有length的功能,这是我们就需要extends这个属性帮我们去鉴定:

interface Props {
        length: number
    }
 
    const calcArray = <T extends Props,>(data:T): number => {
      return data.length // error
    }
 
    calcArray('12') // ok
    calcArray([1,3]) //ok
    calcArray(2) //error

可以看出calcArray(2)会报错,这是因为number类型并不具备length这个属性

typeof

typeof关键字:我们在类型保护的时候讲解了typeof的作用,除此之外,这个关键字还可以实现推出类型,如下图,可以推断中 Props 包含的类型

keyof

keyof关键字: 可以获取一个对象接口的所有key值,可以检查对象上的键是否存在

interface Props {
        name: string;
        age: number;
        sex: boolean
    }
 
    type PropsKey = keyof Props; //包含 name, age, sex
 
    const res:PropsKey = 'name' // ok
    const res1:PropsKey = 'tel' // error
 
    // 泛型中的应用
    const getInfo = <T, K extends keyof T>(data: T, key: K): T[K] => {
        return data[key]
    }
 
    const info = {
        name: '小张',
        age: 7,
        sex: true
    }
 
    getInfo(info, 'name'); //ok
    getInfo(info, 'tel'); //error

索引访问操作符

索引访问操作符:通过 [] 操作符可进行索引访问,可以访问其中一个属性

in

in:映射类型, 用来映射遍历枚举类型

infer

infer:可以是使用为条件语句,可以用 infer 声明一个类型变量并且对它进行使用。如

type Info<T> = T extends { a: infer U; b: infer U } ? U : never;
 
    type Props = Info<{ a: string; b: number }>; // Props类:string | number
 
    type Props1 = Info<number> // Props类型:never

Partial

Partial语法Partial<T> 作用:将所有属性变为可选的 ?

interface Props {
        name: string,
        age: number
    }
 
    const info: Props = {
        name: '小王',
        age: 7
    }
 
    const info1: Partial<Props> = { 
        name: '小徐'
    }

从上述代码上来看,name 和 age 属于必填,对于 info 来说必须要设置 name 和 age 属性才行,但对于 info1来说,只要是个对象就可以,至于是否有name、 age属性并不重要

Required

Required语法Required<T> 作用:将所有属性变为必选的,与 Partial相反

interface Props {
        name: string,
        age: number,
        sex?: boolean
    }
 
    const info: Props = {
        name: '小王',
        age: 7
    }
 
    const info1: Required<Props> = { 
        name: '小徐',
        age: 7,
        sex: true
    }

Readonly

Readonly语法Readonly<T> 作用:将所有属性都加上 readonly 修饰符来实现。也就是说无法修改

interface Props {
        name: string
        age: number
    }
 
    let info: Readonly<Props> = {
        name: '小徐',
        age: 7
    }
 
    info.age = 1 //error read-only 只读属性

从上述代码上来看, Readonly修饰后,属性无法再次更改,智能使用

Record

Record语法:Record<K extends keyof any, T>

作用:将 K 中所有的属性的值转化为 T 类型。

interface Props {
        name: string,
        age: number
    }
 
    type InfoProps = 'JS' | 'TS'
 
    const Info: Record<InfoProps, Props> = {
        JS: {
            name: '小徐',
            age: 7
        },
        TS: {
            name: 'TypeScript',
            age: 11
        }
    }

从上述代码上来看, InfoProps的属性分别包含Props的属性

需要注意的一点是:K extends keyof any其类型可以是:stringnumbersymbol

Pick

Pick语法:Pick<T, K extends keyof T>

作用:将某个类型中的子属性挑出来,变成包含这个类型部分属性的子类型。

interface Props {
        name: string,
        age: number,
        sex: boolean
    }
 
    type nameProps = Pick<Props, 'name' | 'age'>
 
    const info: nameProps = {
        name: '小徐',
        age: 7
    }

从上述代码上来看, Props原本属性包括nameagesex三个属性,通过 Pick我们吧nameage挑了出来,所以不需要sex属性

Exclude

Exclude语法:Exclude<T, U>

作用:将T类型中的U类型剔除。

// 数字类型
    type numProps = Exclude<1 | 2 | 3, 1 | 2> // 3
    type numProps1 = Exclude<1, 1 | 2> // nerver
    type numProps2 = Exclude<1, 1> // nerver
    type numProps3 = Exclude<1 | 2, 7> // 1 2
 
    // 字符串类型
    type info = "name" | "age" | "sex"
    type info1 = "name" | "age" 
    type infoProps = Exclude<info, info1> //  "sex"
 
    // 类型
    type typeProps = Exclude<string | number | (() => void), Function> // string | number
 
    // 对象
    type obj = { name: 1, sex: true }
    type obj1 = { name: 1 }
    type objProps = Exclude<obj, obj1> // nerver

从上述代码上来看,我们比较了下类型上的,当 T 中有 U 就会剔除对应的属性,如果 U 中又的属性 T 中没有,或 T 和 U 刚好一样的情况都会返回 nerver,且对象永远返回nerver

Extra

Extra语法:Extra<T, U>

作用:将T 可分配给的类型中提取 U。与 Exclude相反

type numProps = Extract<1 | 2 | 3, 1 | 2> // 1 | 2

Omit

Omit语法:Omit<T, U>

作用:将已经声明的类型进行属性剔除获得新类型

与 Exclude的区别:Omit 返回的是新的类型,原理上是在 Exclude之上进行的,Exclude是根据自类型返回的

NonNullable

NonNullable语法NonNullable<T> 作用:从 T 中排除 null 和 undefined

ReturnType

ReturnType语法ReturnType<T>

作用:用于获取 函数T的返回类型。

type Props = ReturnType<() => string> // string
    type Props1 = ReturnType<<T extends U, U extends number>() => T>; // number
    type Props2 = ReturnType<any>; // any
    type Props3 = ReturnType<never>; // any

从上述代码上来看, ReturnType可以接受 any 和 never 类型,原因是这两个类型属于顶级类型,包含函数

Parameters

ParametersParameters<T> 作用:用于获取 获取函数类型的参数类型

type Props = Parameters<() => string> // []
    type Props1 = Parameters<(data: string) => void> // [string]
    type Props2 = Parameters<any>; // unknown[]
    type Props3 = Parameters<never>; // never

End

参考:

  • TypeScript 4.0[2]

  • 深入理解 TypeScript[3]

  • 一份不可多得的 TS 学习指南(1.8W字)[4]

以及网上的各种各样的资源。

小结

到此,有关TS的知识就已经说完了,相信掌握了这些知识,你一定会对TS有更深的理解,这篇文章按照自己的理解,进行分类,个人觉得这样的分类比较合理,如果有什么更好的建议,欢迎在评论区指出~

想到自己刚接触TS的时候,是有点抵触的,但随着时间的推移,发现TS真的很香,并且TS也不算是很难,只要你花费一定的时间,在结合与项目,你就会发现真香定律。

猜你喜欢

转载自blog.csdn.net/qq_34093387/article/details/127459704
ts