zoukankan      html  css  js  c++  java
  • TypeScript学习

      TypeScript,简称TS,就是给JS添加了类型系统,你可能会问,为什么要给JS添加类型系统呢?因为JS用法太过灵活了,3+[] 经过一系列的类型转化,竟然得到了字符串“3”, 如果程序出现问题,这种错误很难debug。加了类型,就能解决这类问题吗?是的,因为一旦定义了类型,你就知道,这个类型可以进行哪个操作,比如,number类型,可以进行算术运算,如果进行非法操作,就会报错。类型,就是定义取值的范围,以及对这些值的操作。学习TypeScript就是学习它的类型系统。学习之前,一定要先安装它, mkdir ts-learning && cd ts-learning && npm int -y 新建ts-learning项目, npm install typescript -D安装ts. touch type.ts,就可以写TypeScript代码了。

      TypeScript定义了各式各样的类型,如number, string, boolean等。那怎么给变量指定类型呢?格式为 变量名:变量类型。

    let str: string;
    let a: number = 3;

      如果在声明变量时,进行了初始化操作,像let a:number = 3; a是可以不用指定类型的,TypeScript 能够自动推断出a变量的类型是number类型。let a = 3; 当你把鼠标放到变量a上时,TypeScript 推断它是number类型。

      这也算是一种最佳实践吧,尽量让TypeScript 去推断类型,只有当TypeScript 不能正确地推断时,我们才指定类型。

      使用了类型系统后,程序要怎么运行呢?TS代码是不能直接运行的,要把它编译成JS代码,然后在浏览器或node 中执行。TypeScript 提供了tsc编译器,编译TS代码 到JS,当你npm install typescript 时,同时安装了tsc 编译器。但你要告诉它怎么编译,比如源文件在什么地方,编译到什么地方等等。这些信息放置的地方就是TS的配置文件tsconfig.json, 要把它放到项目的根目录下。执行tsc命令时,它会从项目根目录中寻找tsconfig.json, 然后根据配置信息进行编译。最简单的配置文件如下:

    {
      "compilerOptions": {
        "rootDir": "./src",   /* .ts文件的根目录 */
        "outDir": "dist",     /* 编译后的文件放到哪里 */
        "target": "ES2015",   /* 编译到目标语法*/
        "module": "commonjs"   /* 编译后文件使用哪一种规范*/
      }
    }

      npx tsc 进行编译,项目就多出来了dist目录。node dist/type.js就可以运行程序了。假设粗心大意,给str赋值了数字3,

    let str: string;
    str = 3;

      你会发现str变量下面标红了,鼠标移动上去,看到了报错的原因,数字类型不能赋值给字符串类型。怎么这么神奇呢?npm install typescript,除了安装了刚才说的tsc编译器,还安装了tsserver(TypeScript Language Service), 编辑器或IDE利用tsserver来完成标红提示错误等功能。当你编写TS代码时,编辑器会时时地与tsserver 进行通信,实实编译代码(编译到内存中)。这里要注意的是VS Code使用的TS版本,它默认使用的是Node自带的tsc编译器,当你打开任意一个.ts文件时,在VS Code的右下方,你都能看到这个版本号,

      如果想使用自己安装的TS版本,就点击这个版本号,VS Code会在编辑区的上方弹出一个选择框,点击选择想要的版本就可以了。说的有点远了,现在再执行npx tsc会发生什么?同样报错,tsc 在编译的过程中,它会类型检查(type check),但你也发现, tsc 仍然把错误的TS代码编译成了JS代码。按理说,如果有错,就不应该编译,更不应该输出编译后的JS文件。 幸好,可以在tsconfig.json配置noEmitOnError: true, 如查TS检查出错误,它就不会编译JS代码。

      无论是VS Code编辑器的实时编译,还是tsc 的编译,你都会发现TS给JS加入类型系统的一个好处,就是尽可能早的发现问题,就是尽量把运行时发生的错误在编译时捕获,不用运行代码,也能检测到在程序运行过程中引起问题的代码。

      number, string, boolean都是JS中拥有的类型,行为也类似,就不说了,主要说TypeScript中定义的JS中没有的类型。

      any: 任意类型

      当一个变量是any类型时,它可以被赋值给任何值。

    let str: any;
    str = 2;
    str = 'str';

      同时,any类型的值也可以赋值给任意其它类型。

    let a: any = 'string';
    let b: number = a;

      当使用any类型时,TS不会进行类型检查,代码很容易出错,所以尽量不要用any 类型。

      unknown:不知道的类型

      为什么会有这种类型呢?在极少的情况下,你事先并不知道一个变量要保存什么类型,需要一个类型标注。你可能会说不是有any吗?确实,可以声明为any类型,但一旦声明为any类型,这个变量就不受控制了,就可以对这个变量进行任何操作,但你肯定不想对一个变量进行任意的操作,报错了怎么办?unknown类型和any类型不同之处就是在于控制操作上。当一个变量是unkonwn类型时,你做的事情并不多,如果要使用unknown类型,在使用之前,一定缩小它的类型范围(narrow type)。举个例子,就明白了

    let a: any;
    a = 3;
    console.log(a.toUpperCase());
    
    let b: unknown;
    b = 3;
    // Property 'toUpperCase' does not exist on type 'unknown'. console.log(b.toUpperCase());

      假设事先并不知道a和b的类型,于是分别把a和b标注了any 和unkown类型。先看a(any类型),你可以把任何值赋给它,于是赋值了3,你可以对其进行任何操作,于是调用方法toUpperCase(),实际上数字3并没有toUpperCase()方法,尝试去调用对象不存在的方法,就会导致程序运行时报错。

      再看b,编译器直接报错了,toUpperCase属性不存在unknown类型上。这就是区别。当你声明一个unknown类型的变量时,在对其进行操作(调用方法)之前,编译器会强制你进行向下类型缩小,比如你要对这个类型进行判断,有没有这个属性,这就减小了潜在的运行时报错。缩小类型范围的方法有很多,这里使用typeof, 我们都知道只有字符串才调用toUpperCase()方法,那就判断b是不是字符串

    let b: unknown;
    b = 3;
    if (typeof b === 'string'){
        console.log(b.toUpperCase());
    }

      unknown和any一样,可以表示任意的值 ,但后续的操作却完全不同。any可以进行任意的操作,但unknown却要进行类型缩小,也因此更为安全。如果实在是不知道变量的类型,尽量使用unknown,不要使用any。

      null和undefined 类型

      它们也是JS中已有的类型,之所以把它们拿出来说,是因为,在TypeScript中,null和undefined可以赋值给其它任意类型,而不仅仅是null和undefined类型。

    const x: number = null; 

      竟然没有报错,x是number类型,值却是null,如果用x调用number的方法,肯定报错,最好是null只能赋值给null类型,undefined只能赋给undefined类型。这要配置strictNullChecks来控制null或undefined是否能赋值给其它类型。strictNullChecks: true 表示禁止赋值给其它类型。当在tsconfig.json中写入strictNullChecks: true

    // Type 'null' is not assignable to type 'number'
    const x: number = null; 

      如果变量值就是null,那要明显地标注变量是null类型

    const x: null = null; 

      字面量类型

      TypeScript 还定义了字面量类型,3也是一个类型。变量类型的声明方式是 变量名:类型,如果类型是一个字面量时,就是字面量类型。

    let a: 3;
    let b: 'name';
    let c: false;

      当变量类型是字面量类型时,它的取值也就固定了。a只能赋值为3,b只能赋值为'name',c只能赋值为false。

      数组类型

      在使用数组时,数组中的元素通常是一个类型,所以数组的类型声明是元素的类型后面跟上[]

    let arr: number[]; // arr数组中每一个元素都是number类型

      数组还有一个子类叫元组类型,看一下类型声明就清楚了

    let turple: [number, string];

      [number, string] 就是一个元组类型,[ ]表示它是一个数组类型,但元素的类型声明不是在[ ] 的外面,而是在里面。这表明,数组中元素都是固定的,也就是数组的长度是固定的,并且数组中的每一个元素的类型也是固定的,所以元组就是一个元素类型固定且长度固定的数组。turple 就是2个元素的数组,数组的第一个元素的类型是number,第二个元素的类型是string。

    let arr: number[] = [2, 3, 4, 5];
    let turple: [number, string] = [2, 'name'];

      对象类型

      在TS中有4种方式来标示一个变量(或值)是对象类型

    let obj1: object = {a: 2}; // 小写object 类型
    let a: {b: number} = { b: 12 }  //对象字面量语法,{b: number}类型
    let danger: {} // {} 类型
    let obj: Object; // 大写的object类型。

      先看第一种,小写的object类型。

    let obj1: object = {a: 2};
    // Property 'a' does not exist on type 'object'
    obj1.a;

      获取obj1对象上a属性,发现报错了,object类型上不存在属性a。当声明一个变量是object类型时,只表示它是一个object,而不是null,再也没有其它过多信息了。你对object类型(obj1)什么也做不了。

      再看{} 类型和Object类型。

    let danger: {};
    danger = {};
    danger = {x: 1}
    danger = [];
    danger = 2;
    
    let Obj: Object;
    Obj =1;
    Obj = {x: 1};

      除了null和undefined以外的类型都能赋值{} 类型和Object类型,这两种声明方式没有什么用。

      以上三种声明对象类型的方式,几乎没有什么用,就只剩下对象字面量的声明方式。对象字面量语法声明对象类型,声明的是对象的形状,对象包含哪些属性。这样声明对象类型的方式,叫作structurally typed(duck typing )。structurally typed(duck typing )是一种编程风格,它只关心对象有哪些属性,而不关心它名义上的类型。

      当把一个对象字面量赋值给一个对象类型变量时,TS会执行非常严格的类型检查。对象字面量中的属性,既不能比对象类型中的定义的属性多,也不能比类型中的定义的属性少,必须一一对应,否则报错,这叫excess property check

    let obj: {b: number} = {
        b: 123,
        /* 
        Type '{ b: number; a: number; }' is not assignable to type '{ b: number; }'.
        Object literal may only specify known properties, and 'a' does not exist in type '{ b: number; }'
        */
        a: 456
    }

      但这会带来问题,有的对象有某个属性,有的对象却没有这个属性,有的对象有很多属性,有的对象却没有这些属性,但它们都有共有的属性a,这样怎么声明,可使用?: 标示某个属性可有可无,也可以使用索引表达式 [key: number]: boolean。 key(可以取任意名字)表示对象的属性,是number类型,值是布尔类型。

    let obj: {
        b: number
        c?: string
        [key: number]: boolean
    }

       obj 必须有b属性,且是number类型。c属性可能有也可能没有,如果有c属性,它的值可以是undefined。如果还有其属性,它们必须是number类型,且属性值必须是布尔值。

    obj = {b: 3};
    obj = {b: 2, c:undefined};
    obj = {b: 2, 10: false}
    obj = {10: false} // 报错,没有b属性。

      函数类型

      无论是函数声明,还是函数表达式,在写函数的过程中可以直接定义其参数类型和返回值。

    function sum(a: number, b: number): number {
        return a + b;
    }
    const substract = (a:number, b:number):number => a - b;

      参数a,b都是number类型,参数列表() 后面跟 :number,表示函数的返回值类型,是number类型。如果TS能推断出函数的返回值类型,返回值类型可以省略。

    function sum(a: number, b: number) {
        return a + b;
    }

      当你把鼠标放到sum上时,可以看到TS已经推断出函数的返回值是number类型。如果参数类型也不写呢?

    function sum(a, b) {
        return a + b;
    }

      鼠标放到a上,a是any类型。当TypeScript不能推断一个变量的类型时,它就默认变量的类型为any。这叫隐式any,因为你没有写any这个关键字,变是却是any类型。由于any类型,容易引起问题,所以最好不要让TS做这种类型推断。如果参数确实是any类型,要显示地标示出来。配置项noImplicitAny 就是阻止TS做这种类型推断的。当在tsconfig.json文件中配置noImplicitAny: true时,如果你没有为变量标注类型,而是让TS推断出了它的类型是any时,TS就会报错。

    function sum(a, b) {// Parameter 'a' implicitly has an 'any' type.
        return a + b;
    }

      如果函数没有返回值呢?返回值类型是void

    function print(a: string): void {
        console.log(a);
    }

      当函数抛出错误呢,返回值是never类型,永远不会发生。never类型是所有类型的子类型,你可以把never类型赋值给任意其它类型。

    function erorr(): never {
        throw new Error('Error');
    }

      有时函数参数是默认参数或可能传参,也可能不传参的参数,参数类型用?: 表示。

    function request(url: string, method?: string){}

      函数中还有一个特殊的存在this,函数调用方式不同,this指向就不同,这也是问题的所在,所以函数中有this时,最好声明this的期望类型。在TypeScript中,可以在参数列表中声明this,不过,this要作为第一个参数存在,其它参数放到它后面

    function fancyDate(this: Date, name?: string) {
        return `${this.getDate()}/${this.getMonth()}/${this.getFullYear()}`
    }
    
    fancyDate.call(new Date)

      当函数作为参数传递,或者作为函数返回值时,就不能用这种方式声明类型了,那就需要自定义类型了。

      使用type自定义类型

      使用type自定义类型,格式为:  type 类型名称 = 类型。 以sum函数为例,它的类型是什么呢?两个number类型参数,返回一个number类型,类型就可以这么写

    (a:number, b:number ) => number

      很像箭头函数的语法,参数列表 => 返回值类型。再取个名字Sum,自定义类型就是

    type Sum = (a:number, b:number ) => number;

       Sum就是一个自定义的函数类型,只要一个函数接受两个number类型的参数,并返回number类型,这个函数就可以用Sum 标示。

    let sub: Sum = (a, b) => a - b;

      同时可标注参数或返回值

    function runSum(fn: Sum, a:number, b: number) {
        return fn(a, b);
    }
    function returnSum(): Sum {
        return  (a, b) => a + b ;
    }
    
    returnSum()(7, 8);

      type Sum = (a:number, b:number ) => number; 是Sum类型的简写形式,更完整的形式如下

    type Sum = {
        (a: number, b: number): number
    }

      因为在JS中函数是一个对象,对象就会有属性,声明一个函数类型,也就是声明一个对象类型,就可以用对象字面量的方式声明它。

      使用Type 来定义类型的时候,有两个重要概念,type union(联合类型),type insertection(交叉类型)。

      type union(联合类型)

      type union(联合类型):两个或多个类型使用 | 联合在一起,形成一个新的类型。可以把两个基本类型联合起来

    type StrOrNum =  string | number;

       StrOrNum 或是string类型,或是number类型。使用这个联合类型时,只能使用string和number类型的共用的属性,两者很少共有属性。可以把两个对象类型联合起来

    type Cat = {name: string, purrs: boolean}
    type Dog = {name: string, barks: boolean, wags: boolean}
    type CatOrDogOrBoth = Cat | Dog

      当你看到一个类型是CatOrDogOrBoth的时候, 你想到了什么?保险起见,我只使用它们共有的属性name,这个类型肯定有一个name属性,其它属性有没有,不敢保证。反过来想,既然这个类型是CatOrDogOrBoth,我可以给它赋一个cat值,也可以给它赋一个Dog值,共有的值也可以赋给它。

    let c:CatOrDogOrBoth = {name: 'cat', purrs: false } // Cat类型
    let d:CatOrDogOrBoth = {name: 'dog', barks: false, wags: false } // dog类型
    let cdBoth: CatOrDogOrBoth = { name: 'both', barks: true, purrs: true, wags: true}

      类型联合时要注意,如果两个类型中有相同的属性,但属性却是不同的类型时,属性的两个类型也进行联合(union)

    type Product = {
        id: number,
        name: string,
        price?: number
    };
    type Person = {
        id: string,
        name: string,
        city: string
    };
    
    // Product 和 Person 联合之后的结果
    type UnionType = {
        id: number | string,
        name: string
    };

      UnionType就是Product和Person类型进行联合后生成的新类型。在UnionType中,id属性的类型是联合类型 number | string ,就是因为id 在Product中是number 类型,但是在Person类型中是string类型.  name属性在两个类型中都是string, 所以在新的联合类型中,name属性也是string

      type insertection(交叉类型)

      type insertection(交叉类型):两个或多个类型使用 & 联合在一起,形成一个新的类型。

    type strAndnum =  string & number;

       strAndnum,既是string,又是number,新的类型集合了string类型 和number类型的所有属性。正是由于把所有的属性都集合到了一起,你会发现,没有一个值既是string,又是number,所以这个类型没有意义。通常,&用于对象类型。

    type Person = {
        id: string;
        name: string;
        city: string;
    }
    
    type Employee = {
        company: string;
        dept: string
    }
    
    type PersonAndEmpoyee = {
        id: string;
        name: string;
        city: string;
        company: string;
        dept: string
    }

      PersonAndEmpoyee就是PersonEmpoyee进行交叉生成的新的类型。你可以看到,type insertection(交叉类型) 就是把所有对象类型的属性全部组合到一起,允许你使用所有的属性,而不是共有类型。

       当给一个交叉类型进行赋值时,值必须包含每一个类型中都定义的属性

    let bob: Person & Employee = {
        id: "bsmith", name: "Bob", city: "London",
        company: "Acme Co", dept: "Sales"
    };

      如果两个要交叉类型有相同的属性时,这时要注意。如果相同的属性,类型也相同,这没有问题。给Empoyee再声明一个id:string属性,Person & Employee 的结果还是一样  

     

       但是如果类型不同,这个属性的类型,是两个不同类型进行交叉,分别给Person和Employee 定义一个contact属性,类型不同

    type Person = {
        id: string;
        contact: number;
    }
    
    type Employee = {
        id: string;
        contact: string;
    }

      Person & Employee

    type PersonAndEmpoyee = {
        id: string;
        contact: number & string;
    }

       这时就出现问题了,没有一个值,它既是number 又是string. 解决办法,就是相同的属性不要使用原始类型,要使用对象。  

    type Person = {
        id: string;
        contact: {phone: number};
    }
    
    type Employee = {
        id: string;
        contact: {name: string}
    }
    
    type PersonAndEmpoyee = {
        id: string;
        contact: {phone: number} & {name: string};
    }

        

      

  • 相关阅读:
    Understanding about Baire Category Theorem
    Isometric embedding of metric space
    Convergence theorems for measurable functions
    Mindmap for "Principles of boundary element methods"
    Various formulations of Maxwell equations
    Existence and uniqueness theorems for variational problems
    Kernels and image sets for an operator and its dual
    [loj6498]农民
    [luogu3781]切树游戏
    [atAGC051B]Three Coins
  • 原文地址:https://www.cnblogs.com/SamWeb/p/15353011.html
Copyright © 2011-2022 走看看