TypeScript語法基礎

  • 2019 年 10 月 3 日
  • 筆記

什麼是TypeScript?

TypeScript是微軟開發的一門程式語言,它是JavaScript的超集,即它基於JavaScript,拓展了JavaScript的語法,遵循ECMAScript規範(ES6/7/8+)。

TypeScript = Type + Script(標準JS),它可以編譯成純JavaScript,已經存在的JavaScript也可以不加改動地在TS的環境上運行。

目前, Angular 已經使用 TypeScript 重構了程式碼,另一大前端框架 Vue 的3.0版本也將使用 TypeScript 進行重構。在可預見的未來,TypeScript 將成為前端開發者必須掌握的開發語言之一。

為什麼要使用TypeScript?

  1. 提升開發效率。
  2. 提升可維護性。
  3. 提升線上運行品質。TS有編譯期的靜態檢查,加上IDE的智慧糾錯,儘可能的將BUG消滅在編譯器上,線上運行時品質更穩定可控。
  4. 可讀性強,適合團隊協作

TypeScript開發環境

npm install -g typescript  // 安裝ts編譯器  tsc hello.ts // 手動編譯ts文件,會生成同名js文件  tsc --init  // 生成tsconfig.js文件

當然,我們可以配置webpack,開啟node服務,進行熱更新開發。

TypeScrip數據類型

學習數據類型前,要先明白兩個概念:

強類型和弱類型

強類型指一個變數一旦聲明,就確定了它的類型,此後不能改變它的類型。弱類型可以隨便轉換。TypeScript是強類型語言,JavaScript是弱類型語言。

靜態類型和動態類型

靜態類型語言:編譯階段檢查所有數據的類型。動態類型語言:將檢查數據類型的工作放在程式的執行階段,也就是說,只有在程式執行時才能確定數據類型。

基本類型

在ES6的基礎上,新增了void、any、never、元組、枚舉、高級類型。

布爾、數字、字元串

let bool: boolean = true;  let num: number = 10;  let str: string = "abc";  let abc: number | boolean | null; // 可以為一個變數聲明多種類型,除非有特殊需求,一般不建議這樣做。

另外,ts字元串有一些特性:

1.多行字元串  ·aa  bb  cc·  反引號包著,直接換行即可,編譯成js後,會加上n換行符  2.自動拆分字元串  在調用函數時,可以將模板字元串拆分,並且不需要寫小括弧  function test(a:string,b:string,c:number){console.log(a,b,c)}  test`string...${name}...${age}...` // 注意調用方式,沒有括弧,直接反引號  會將字元串拆分為第一個參數,name拆分成第二個參數,age為第三個參數

數組

TypeScript的數組,所有元素只能是同一種數據類型。

let arr1: number[] = [1,2,3];  let arr2: Array<number> = [4,5,6]; // 數組的第二種聲明方式,與前面等價  let arr3: string[] = ["hello","array","object"];

元組

元組是特殊的數組,限制了元素的個數和類型。

let tuple: [number,string,boolean] = [10,"hello",true];  // tuple.push(5); // 元組越界,但不會報錯。原則上不能對tuple push,這應該是一個缺陷  // console.log(tuple[3]); // 新增的元素無法訪問。

函數

  • 函數的聲明定義(三種方式)
  • 函數傳參
  1.  可選參數必須放在必選參數的後面
  2.  使用ES6的默認參數,不需要聲明類型
  3.  使用ES6的剩餘參數,需要聲明類型
// 方式一 箭頭函數:聲明和定義分開  let compute: (x:number, y:number) => number; // 函數聲明,規定了傳入參數、返回值的數據類型  compute = (a, b) => a+b;  // 函數定義時,參數名稱不必與聲明時的相同  // 方式二:箭頭函數:聲明的同時定義  let add = (x:number, y:number) => { return x+y }; // 返回值的類型可省略,這利用了ts的類型推斷功能  // 方式三:function關鍵字:聲明和定義分開  function add (x: number,y: number): number;  // 方式四:function關鍵字:聲明的同時定義  function add (x: number,y: number): number{ retrun x+y; }  //函數傳參:  function add789(x: number, y?: number, z=1, ...rest: number[]) {      console.log(rest);      return y ? x+y : x  }

對象

// 正確的寫法  let obj1: {x:number, y:number} = {x: 1, y: 2};  obj1.x = 3;    // 不建議的寫法  let obj: object = {x: 1, y: 2};  obj.x = 3; // 報錯,因為定義的時候繞過了聲明檢查,此時不知道是什麼類型。

symbol

let s1: symbol = Symbol();  let s2 = Symbol();

undefind、null

let un: undefined = undefined;  let nu: null = null;  nu = null; // 這樣是允許的,需要將tsconfig中“strictNullChecks”置為false  un = undefined;

void

是一種操作符,可以讓任意一個表達式返回undefined。之所以引進void,是因為undefined不是一個保留字,可以在局部作用域內將其覆蓋掉。

let noReturn = () => {};

any

any 表示變數可以為任何類型,在TS中一般不用它。如果使用它,也便失去了使用TS的意義,與前面不建議為變數聲明多種類型是一個道理。

let x: any;  x = 1;  x = "str";

never

表示永遠不會有返回值的類型

let error = () => {      throw new Error("errors")  };  let endless = () => {      while(true)      {}  };  // 以上兩個例子永遠不會有返回值

枚舉類型 enum

枚舉主要來定義一些常量,方便記憶,減少硬編碼,增加可讀性。

基本使用:

// 數字枚舉  enum Role {      Reporter, // 默認從0開始      Developer=5, // 也可指定某個值      Maintainer,  }  console.log(Role); // 可以看到數據結構,能夠進行反向映射,即通過值來訪問成員  console.log(Role.Developer);  // 訪問枚舉成員    // 字元串枚舉 不可以進行反向映射  enum message {      success = "成功了",      fail = "失敗了"  }  console.log(message);    // 異構枚舉,將數字和字元串混用  enum Answer {      N = 0,      Y = "yes"  }

    注意:不能修改枚舉成員的值

枚舉成員的分類:

枚舉成員的分類:      (1)常量枚舉成員      (2)對已有枚舉成員的引用      (3)常量表達式      (4)非常量表達式。這種成員的值不會在編譯階段確定,在執行階段才會有  例:  enum Char {      a,      b = 9,      c = message.success,      d = b,      e = 1 + 3,      f = Math.random(),      g = "123".length,  }  console.log(Char);

常量枚舉和枚舉類型

// 常量枚舉 用const聲明的枚舉都是常量枚舉。特性:在編譯階段被移除,編譯後不會出現  // 作用:當我們不需要對象,只需要對象的值的時候  const enum Month{      Jan,      Feb,      Mar,  }  let month = [Month.Jan,Month.Feb,Month.Mar];  console.log(month);    // 枚舉類型 枚舉可以作為一種類型  let e: Role = 2;  let e1: Role.Developer = 12;  // 數字枚舉類型與number類型相互兼容,因此可以複製  console.log(e,e1); // 按照Role的類型去聲明新變數    let g1: message.success = "hello"; // 報錯,字元串枚舉類型message.success與string類型不兼容    e === e1; // 可比較  e === g1; // 不可比較,因為類型不一樣

interface介面

介面可以用來約束對象、函數、類的結構和類型,是一種契約,並且聲明之後不可改變。

1.定義 (interface關鍵字)

interface List {      id: number;      name: string;  }    interface Result {      data: List[]; // 表示由List介面組成的數組  }    function render(result:Result) {      result.data.forEach((value)=>{          console.log(value);      })  }    let result = {      data:[          {id:1,name:"a"},          {id:2,name:"b"},      ],  };  render(result);

2.內部規範了什麼?

通過上述例子,看到介面規範了成員名稱、成員的的類型、值的類型。

此外,還可以規範成員屬性。

3.成員屬性

可選屬性和只讀屬性

interface List {      readonly id: number; // readonly表示只讀屬性      name: string;      age?: number; // ?表示可選屬性  }

4.索引簽名

當不確定介面中有多少屬性的時候,可以用索引簽名。

格式:[x: attrType]: valueType  分別規定了成員的類型和值的類型,即通過什麼來索引和訪問的值的類型。

一般通過數字和字元串來索引,也可以兩者混合索引。

// 用數字索引  interface StringArray {      [index: number]: string; // 表示,用任意的數字去索引StringArray,都會得到一個string。這就相當於聲明了一個字元串類型的數組  }  let chars: StringArray = ["A","B"]; // 此時,chars就是一個字元串數組,我們可以用下標去訪問每個元素  console.log(chars,chars[0]);    // 用字元串和數字混合索引  interface Names {      [x: string]: string; // 用任意的字元串去索引Names,得到的結果都是string。      // y: number; // 此時不能聲明number類型的成員      // [y: number]: number // 報錯,因為x和y的值string和number類型不兼容      [z: number]: any; // 兩個簽名的返回值類型之間要相互兼容。為了能保持類型的兼容性。  }  let names: Names = {"ming":"abc",1:"45"};  console.log(names[1],names["ming"]); // 通過數字索引、通過字元串索引

※ 注意值的類型要兼容

  (1)索引簽名和普通成員

    如果設置了[x: string]: string,不能再設置y: number。如果設置了[x: string]: number不能再設置y: string

  (2)索引簽名和索引簽名

    如果多個索引簽名的值不同,要注意相互兼容,比方any和string

5.函數傳參時如何繞過類型檢查

如果在接收的後端數據中,比約定好的介面多了一個欄位,能否通過類型檢查?會不會報錯?

let result = {      data:[          {id:1,name:"a",sex:"man"},          {id:2,name:"b"},      ],  };  render(result); // 這樣是不會報錯的,只要滿足介面約定的必要條件即可    render({      data:[          {id:1,name:"a",sex:"man"},          {id:2,name:"b"},      ],  }); // 但如果這樣調用,會報錯,因為無法通過sex:"man"的類型檢查。這時候需要用其他方法

我們有三種方法:

  1. 通過介面定義變數,函數調用時傳入變數名(只對必要的約定條件進行檢查,多餘的數據不做檢查)
  2. 類型斷言(所有約定都不做類型檢查,失去了ts類型檢查的意義)
  3. 索引簽名

 第一種方法已經在上面做了示例,我們看後面兩種方法如何做:

// 類型斷言  render({      data:[          {id:"b",name:3,sex:"man"},          {id:2,name:"b"},      ],  }as Result); // 明確告訴編譯器,數據符合Result,這樣,編譯器會繞過類型檢查  render(<Result>{      data:[          {id:1,name:"a",sex:"man"},          {id:2,name:"b"},      ],  }); // 與上等價,但在React中容易引起歧義。不建議使用    // 索引簽名  interface List {      id: number;      name: string;      [x: string]: any; // 字元串索引簽名。用任意字元串去索引List,可以得到任意的結果,這樣List介面可以支援多個未知屬性  }

在什麼場景下用什麼方法,需要我們熟知這三種方法的特性

6.介面和函數

介面可以用來定義函數的傳參、返回值的類型

interface Add1 {      (x: number,y: number): number;  }  let add1: Add1 = (a,b) => a+b;

此外,還可以用類型別名來定義函數

type Add2 = (x: number,y: number) => number;  let add2: Add2 = (a,b) => a+b; // 聲明+定義

我們再來總結一下函數的聲明定義方式:

  1. 普通聲明定義(function、箭頭函數)
  2. 介面定義類型
  3. 類型別名

另外,介面內也可以定義函數

// 混合類型介面  interface Lib {      abc(): void;      version: string;      doSomething(): void;  }    function getLib(){      let lib: Lib = {          abc: ()=>{},          version: "1.0",          doSomething: ()=>{}      };      // let lib: Lib = {} as Lib; // 定義的時候,這種方式更方便      lib.version = "1.0";      lib.doSomething = () => {};      return lib;  }  let lib1 = getLib();  console.log(lib1,lib1.version,lib1.doSomething());

class類

關於類的成員:

1.屬性必須有類型註解

2.屬性必須有初始值

3.屬性修飾符:

  (1)公有 public

    所有成員默認都是public。可以通過各種方式訪問。

  (2)私有 private

    私有成員只能在類中被訪問,不能被實例和子類訪問。如果給構造函數加上私有屬性,表示這個類既不能被實例化也不能被繼承。

  (3)受保護 protected

    受保護成員只能在類和子類中訪問,不能通過它們的實例訪問。如果給構造函數加上受保護屬性,表示這個類不能被實例化只能被繼承。也就是聲明了一個基類。

  (4)靜態 static

    靜態成員只能通過類名和子類名訪問,不能被實例訪問。

  (5)只讀 readonly

    只讀成員不能被修改。

4.以上屬性除了可以修飾成員,也可以修飾構造函數中的參數(static除外)。這樣可以省去構造函數之中外的類型註解,簡化程式碼。

class Dog{      constructor(name: string){          this.name = name; // 屬性必須賦初值      }      name: string; // 必須要為屬性添加類型註解。      run(){          console.log("running");          this.pri(); // 只能在類內部訪問私有成員          this.pro();      }      private  pri(){ // 私有成員只能被類本身調用,不能被類的實例和子類調用          console.log("pri是dog類的私有屬性");      }      protected pro(){ // 受保護成員只能在類和子類中訪問          console.log("pro是dog類的受保護屬性");      }      readonly logs: string = "new";      static food: string = "food"; // 靜態修飾後,只能通過類名調用,不能被子類和實例調用。靜態成員可以被繼承  }  let dog = new Dog("dog1");  dog.run();  console.log(Dog.food); // 通過類名訪問靜態成員

抽象類和多態

所謂抽象類(abstract),是只能被繼承,不能被實例化的類。

 抽象方法

在抽象類中,不必定義方法的具體實現,這就構成了抽象方法。在抽象類中使用abstratct關鍵字修飾後,不能定義該方法的具體實現。

抽象方法的好處是:實現多態。

interface AnimalParam{      bigClass: string,      environment: string,      [x: string]: string;  }  abstract class Animal{  // 抽象類用abstract關鍵字修飾      constructor(params: AnimalParam) {  // 構造函數參數使用介面是為了其子類在定義的時候方便傳參          this.bigClass = params.bigClass;          this.environment = params.environment;      }      bigClass: string;      environment: string;      abstract sleep(): void;  }    class Dogs extends Animal{      constructor(props: AnimalParam){          super(props);          this.name = props.name;      }      name: string;      run(){          console.log("running");      }      sleep() {          console.log("dog sleep")      }  }  class Cat extends Animal{      sleep(): void {          console.log("cat sleep")      }  }  let dog1 = new Dogs({bigClass:"a",environment:"b",name:"xiaoqi"});  let cat = new Cat({bigClass:"a",environment:"b"});    let animals: Animal[] = [dog1,cat];  animals.forEach(i=>{      i.sleep();  });

 this

我們可以在方法中返回this,可以進行鏈式調用,非常方便。

class WorkFlow{      step1(){          return this;      }      step2(){          return this;      }  }  class Myflow extends WorkFlow{      next(){          return this;      }  }  console.log(new WorkFlow().step1().step2());  console.log(new Myflow().next().step1().step2());

類和介面

類類型介面

  • 介面約束類成員有哪些屬性,以及它們的類型
  • 類在實現介面約束的時候,必須實現介面中描述的所有內容。可以多,不可以少
  • 介面只能約束類的公有成員,即不可以將介面中規定的成員置為非公有的屬性
  • 介面不能約束類的構造函數
interface Human {      name: string;      eat(): void;  }  class Asian implements Human{      constructor(name: string){          this.name = name;      }      name: string;      eat(){}      sleep(){}  }

介面繼承介面

interface  Man extends Human{      run(): void  }  interface Child {      cry(): void  }  interface Boy extends Man,Child{} // 多繼承,將多個介面合併成一個介面

介面繼承類

可以理解為,將類轉化成介面。介面集成類的時候,不僅抽離了公有成員,也抽離了私有成員、受保護成員。

如何理解呢?這麼做的目的是限定介面的使用範圍,並不會真正為這個介面添加類的私有和受保護屬性,而這個限定範圍就是:只能由子類來實現這個介面。

class Auto{      state = 1;      private state2 = 0;      protected state3 = 3;  }  interface AutoInterface extends Auto{} // 介面繼承類    class C implements AutoInterface{ // C在實現這個介面的時候,無法實現介面中的私有成員和受保護成員,因此報錯      state = 1  }  class Bus extends Auto implements AutoInterface{ // Auto的子類Bus,遵循AutoInterface介面      showMsg(){          // console.log(this.state2);          console.log(this.state3);      }  }    let bus = new Bus();  bus.showMsg();  console.log(bus);