TypeScript 基礎精粹

原文地址:TypeScript 基礎精粹javascript

基礎筆記的github地址:github.com/qiqihaobenb… ,能夠watch,也能夠star。
java


類型注意事項

數組類型

有兩種類型註解方式,特別注意第二種使用 TS 內置的 Array 泛型接口。git

let arr1: number[] = [1,2,3]
// 下面就是使用 TS 內置的 Array 泛型接口來實現的
let arr2: Array<number | string> = [1,2,3,"abc"]
複製代碼

元組類型

元組是一種特殊的數組,限定了數組元素的個數和類型github

let tuple: [number, string] = [0, "1"];
複製代碼

須要注意元組的越界問題,雖然能夠越界添加元素,可是仍然是不能越界訪問,強烈不建議這麼使用typescript

tuple.push(2)  // 不報錯
console.log(tuple) // [0, "1", 2] 也能都打印出來
console.log(tuple[2]) // 可是想取出元組中的越界元素,就會報錯元組長度是2,在index爲2時沒有元素
複製代碼

函數類型

函數類型能夠先定義再使用,具體實現時就能夠不用註明參數和返回值類型了,並且參數名稱也不用必須跟定義時相同。數組

let compute: (x: number, y: number) => number;
compute = (a, b) => a + b;
複製代碼

對象類型

對象若是要賦值或者修改屬性值,那麼就不能用簡單的對象類型,須要定義完整的對象類型app

let obj: object = { x: 1, y: 2 };
obj.x = 3; // 會報錯,只是簡單的定義了是object類型,可是裏面到底有什麼屬性沒有標明

// 須要改爲以下的對象類型定義
let obj: { x: number; y: number } = { x: 1, y: 2 };
obj.x = 3;
複製代碼

symbol 類型

symbol 類型能夠直接聲明爲 symbol 類型,也能夠直接賦值,跟 ES6 同樣,兩個分別聲明的 symbol 是不相等的。dom

let s1: symbol = Symbol();
let s2 = Symbol();
console.log(s1 === s2)  // false
複製代碼

undefined 、null 類型

變量能夠被聲明爲 undefined 和 null ,可是一旦被聲明,就不能再賦值其餘類型。分佈式

let un: undefined = undefined;
let nu: null = null;
un = 1 // 會報錯
nu = 1 // 會報錯
複製代碼

undefined 和 null 是任何類型的子類型,那就能夠賦值給其餘類型。可是須要設置配置項 "strictNullChecks": false函數

// 設置 "strictNullChecks": false
let num: number = 123;
num = undefined;
num = null;

// 可是更建議將 num 設置爲聯合類型
let num: number | undefined | null = 123;
num = undefined;
num = null;
複製代碼

枚舉類型

枚舉分爲數字枚舉和字符串枚舉,此外還有異構枚舉(不推薦)

數字枚舉

枚舉既能經過名字取值,又能經過索引取值,咱們具體看一下是怎麼取到的。

enum Role {
  Reporter = 1,
  Developer,
  Maintainer,
  Owner,
  Guest
}
Role.Reporter = 2 // 枚舉成員是隻讀的,不能修改從新賦值

console.log(Role)
//打印出來:{1: "Reporter", 2: "Developer", 3: "Maintainer", 4: "Owner", 5: "Guest", Reporter: 1, Developer: 2, Maintainer: 3, Owner: 4, Guest: 5}
//咱們看到打印出來是一個對象,對象中有索引值做爲 key 的,有名字做爲 key 的,因此枚舉既能經過名字取值,又能經過索引取值

// 看一下 TS 編譯器是怎麼用反向映射實現枚舉的。
"use strict";
var Role;
(function (Role) {
    Role[Role["Reporter"] = 1] = "Reporter";
    Role[Role["Developer"] = 2] = "Developer";
    Role[Role["Maintainer"] = 3] = "Maintainer";
    Role[Role["Owner"] = 4] = "Owner";
    Role[Role["Guest"] = 5] = "Guest";
})(Role || (Role = {}));
複製代碼

字符串枚舉

字符串枚舉只能經過名字取值,不能經過索引取值。

enum Message {
  Success = '成功',
  Fail = '失敗'
}
console.log(Message)
// 打印出來:{Success: "成功", Fail: "失敗"}
// 咱們看到只有名字做爲 key ,說明字符串枚舉不能反向映射
複製代碼

常量枚舉

用 const 聲明的枚舉就是常量枚舉,會在編譯階段被移除。以下代碼編譯後 Month 是不產生代碼的,只能在編譯前使用,當咱們不須要一個對象,可是須要一個對象的值的時候,就可使用常量枚舉,這樣能夠減小編譯後的代碼。

const enum Month {
  Jan,
  Feb,
  Mar
}
let month = [Month.Jan, Month.Feb, Month.Mar];
複製代碼

異構枚舉

數字和字符串枚舉混用,不推薦

enum Answer {
  N,
  Y = 'Yes',
  // C, // 在字符串枚舉成員後面的枚舉成員必須賦一個初始值
  // X = Math.random() // 含字符串成員的枚舉中不容許使用計算值
}
複製代碼

枚舉成員注意點

  • 枚舉成員是隻讀的,不能修改從新賦值
  • 枚舉成員的分爲 const member 和 computer member
  • 常量成員(const member),包括沒有初始值的狀況、對已有枚舉成員的引用、常量表達式,會在編譯的時候計算出結果,以常量的形式出如今運行時環境
  • 計算成員(computer member),須要被計算的枚舉成員,不會在編譯階段進行計算,會被保留到程序的執行階段
  • 在 computed member 後面的枚舉成員,必定要賦一個初始值,不然報錯
  • 含字符串成員的枚舉中不容許使用計算值(computer member),而且在字符串枚舉成員後面的枚舉成員必須賦一個初始值,不然會報錯(見上面的異構類型)
  • 數字枚舉中,若是有兩個成員有一樣索引,那麼後面索引會覆蓋前面的(見下面的枚舉 number )
// 枚舉成員
 enum Char {
   // const member 常量枚舉,會在編譯階段計算結果,以常量的形式出如今運行時環境
   a,
   b = Char.a,
   c = 1 + 3,

   // computed member 須要被計算的枚舉成員,不會在編譯階段進行計算,會被保留到執行階段
   d = Math.random(),
   e = '123'.length,
   // 在 computed member 後面的枚舉成員,必定要賦一個初始值,不然報錯
   f = 1
 }
 console.log(Char)

 // 枚舉 number
 enum number { a = 1, b = 5, c = 4, d }
 console.log(number) //打印出{1: "a", 4: "c", 5: "d", a: 1, b: 5, c: 4, d: 5}
 // b賦初始值爲5,c賦初始值爲4,按照索引遞增,d的索引就是5,索引相同時,後面的值覆蓋前面的,因此5對應的 value 就是d
複製代碼

枚舉和枚舉成員做爲單獨的類型

有如下三種狀況,(1)枚舉成員都沒有初始值、(2)枚舉成員都是數字枚舉、(3)枚舉成員都是字符串枚舉

  • 變量定義爲數字枚舉類型,賦值任意 number 類型的值都是能夠的(能夠超出枚舉定義的數字範圍),對枚舉沒有影響,可是不能賦值字符串等。
  • 不一樣的枚舉類型是不能比較的,不過同一個枚舉類型是能夠比較的,可是同一個枚舉類型的不一樣枚舉成員是不能比較的
  • 變量定義爲枚舉類型,甚至就算定義爲枚舉類型的某個具體成員的類型,賦值也是對枚舉沒有影響的。(以下,E和F的結果仍是不變的)
  • 字符串枚舉類型的賦值,只能用枚舉成員,不能隨意賦值。(若是下F)
enum E { a, b } // 枚舉成員都沒有初始值
 enum F { a = 1, b = 5, c = 4, d } // 枚舉成員都是數字枚舉
 enum G { a = 'apple', b = 'banana' } // 枚舉成員都是字符串枚舉

 // 變量定義爲數字枚舉類型,賦值任意number類型的值都是能夠的,對枚舉沒有影響,可是不能賦值字符串等。
 let e: E = 3
 let f: F = 3
 // e === f // 不一樣的枚舉類型是不能比較的,會報錯
 console.log(E,F,e,f) // 打印:{0: "a", 1: "b", a: 0, b: 1}, {1: "a", 4: "c", 5: "d", a: 1, b: 5, c: 4, d: 5}, 3, 3
 // 可見變量定義爲E,F賦值,對E,F枚舉自己沒有影響

 let e1: E = 3
 let e2: E = 3
 console.log(e1 === e2) // 同一個枚舉類型是能夠比較的,結果爲true

 let e3: E.a = 3
 let e4: E.b = 3
 // e3 === e4 // 同一個枚舉類型的不一樣枚舉成員是不能比較的,會報錯
 console.log(E,E.a,E.b,e3,e4) // 打印:{0: "a", 1: "b", a: 0, b: 1} 0 1 3 3 ,可見變量定義爲E.a,E.b賦值,對E以及E.a,E.b枚舉自己沒有影響

 //字符串枚舉類型的賦值,只能用枚舉成員,不能隨意賦值。
 let g1: G = 'abc' // 會報錯
 let g2: G = G.a // g2能賦值G.a或者G.b
 let g3: G.a = G.a // g2 只能賦值G.a
複製代碼

接口類型

接口約束對象、函數、類的結構

對象類型接口

對象冗餘字段

對象類型接口直接驗證有冗餘字段的對象字面量時會報錯,這種冗餘字段有時是不可避免的存在的。

interface List {
   id: number;
   name: string;
 }
 interface Result {
   data: List[];
 }

 function render(result: Result) {
   result.data.forEach((value) => {
     console.log(value.id,value.name)
   })
 }

 render({
   data: [
     {id: 1, name: 'A',sex: 'male'},
     {id: 2,name: 'B'}
   ]
 });
 // 這就是對象類型接口直接驗證有冗餘字段的「對象字面量」,上面render中會有報錯,說對象只能指定已知屬性,而且"sex"不在類型"List"中
複製代碼

解決方法一:在外面聲明變量 result ,而後把 result 傳入 render 函數,避免傳入對象字面量。

// 把字面量先賦值給一個變量這樣就能繞過檢測
 let result = {
   data: [
     {id: 1, name: 'A',sex: 'male'},
     {id: 2,name: 'B'}
   ]
 }
 render(result);
複製代碼

解決方法二: 用類型斷言(兩種 as 和尖括號),可是若是對象字面中都沒有符合的,仍是會報錯,能夠用 as unknown as xxx

render({
   data: [{ id: 1, name: "A", sex: "male" }, { id: 2, name: "B" }]
 } as Result);

 // 可是若是傳入的對象字面量中沒有一項是符合的,那用類型斷言仍是會報錯
 render({
   data: [{ id: 1, name: "A", sex: "male" }]
 } as Result); // 仍是會報錯屬性"data"的類型不兼容

 // 如今就須要這麼寫,用 as unknown as xxx
 render({
   data: [{ id: 1, name: "A", sex: "male" }]
 } as unknown as Result);

複製代碼

解決方法三:用字符串索引簽名

interface List {
   id: number;
   name: string;
   [x:string]: any;
 }
 // 這樣對象字面量就能夠包含任意多個字符串屬性了。
複製代碼
接口屬性可定義爲只讀屬性和可選屬性
interface List {
   readonly id: number; // 只讀屬性
   name: string;
   age?: number; // 可選屬性
 }
複製代碼
可索引類型

不肯定一個接口中有多少屬性時,可使用可索引類型。分爲數字索引簽名和字符串索引簽名,若是接口定義了某一種索引簽名的值的類型,以後再定義的屬性的值必須是簽名值的類型的子類型。能夠同時使用兩種類型的索引,可是數字索引的返回值必須是字符串索引返回值類型的子類型。

interface Names {
   [x: string]: number | string;
   // y: boolean; // 會報錯 boolean 不會賦值給字符串索引類型,由於字符串索引簽名的類型是 number | string,因此以後再定義的屬性必須是簽名值類型的子類型
   [z: number]: number; // 字符串索引簽名後也能定義數字索引簽名,數字索引的返回值必須是字符串索引返回值類型的子類型
 }
複製代碼

函數類型接口

interface Add {
  (x: number, y: number): number;
}
// 跟變量聲明是等價的:let Add: (a: number, b: number) => number
let add4: Add = (a,b) => a + b
複製代碼

混合接口

混合接口,須要注意看一下,接口中的屬性沒有順序之分,混合接口不須要第一個屬性是匿名函數。

interface Lib {
  version: string;
  ():void;
  doSomething():void;
}
// 須要用到類型斷言
let lib: Lib = (() => {}) as Lib; lib.version = '1.0' lib.doSomething = () => {}
複製代碼

接口繼承

// 如下是接口繼承的例子
interface Human {
  name: string;
  eat(): void;
}
interface Man extends Human {
  run(): void
}
interface Child {
  cry():void
}

interface Boy extends Man, Child {}
let boy: Boy = {
  name: '',
  run(){},
  eat(){},
  cry(){}
}
複製代碼

函數類型相關

定義 TS 函數的四種方式,第一種方式能夠直接調用,可是後三種就須要先實現定義的函數再調用

// 第一種,直接聲明
function add1 (x:number, y:number):number {
  return x + y
}
// 應用時形參和實參一一對應
add1(1, 2)

// 第二種 變量聲明
let add2: (x:number, y:number) => number
// 應用以下
add2  = (a, b) => a + b
add2(2, 2)

// 第三種 類型別名
type Add3 = (x: number, y: number) => number
// 應用以下
let add3: Add3 = (a, b) => a + b
add3(3, 2)


// 第四種 接口實現
interface Add4 {
  (x: number, y: number): number;
}
// 跟變量聲明是等價的:let Add4: (a: number, b: number) => number
let add4: Add4 = (a,b) => a + b
add4(4, 2)
複製代碼

可選參數

可選參數必須位於必選參數以後,便可選參數後面不能再有必選參數

// y後面不能再有必選參數,因此d會報錯
// function add5(x:number, y?:number, d:number) {

// 正確以下
function add5(x:number, y?:number) {
  return y? y + x: x
}
add5(1)
複製代碼

參數默認值

帶默認值的參數不須要放在必選參數後面,但若是帶默認值的參數出如今必選參數前面,必須明確的傳入 undefined 值來得到默認值。在全部必選參數後面的帶默認值的參數都是可選的,與可選參數同樣,在調用函數的時候能夠省略。

function add6 (x: number, y = 0, z:number,q = 1) {
  return x +y + z +q
}
// 第二個參數必須傳入undefined佔位
add6(1,undefined,2)
複製代碼

函數重載

要求定義一系列的函數聲明,在類型最寬泛的版本中實現重載, TS 編譯器的函數重載會去查詢一個重載的列表,而且從最開始的一個進行匹配,若是匹配成功,就直接執行。因此咱們要把大機率匹配的定義寫在前面。

函數重載的聲明只用於類型檢查階段,在編譯後會被刪除。

function add8(...rest: number[]):number
function add8(...rest: string[]):string
function add8(...rest: any[]):any {
  let first = rest[0]
  if(typeof first === 'string') {
    return rest.join('')
  }
  if(typeof first === 'number') {
    return rest.reduce((pre,cur) => pre + cur)
  }
}
add8(1,2,3) // 6
add8('1','2','3') // '123'
複製代碼

類屬性和方法注意點

  • 類屬性都是實例屬性,不是原型屬性,而類方法都是原型方法
  • 實例的屬性必須具備初始值,或者在構造函數中初始化,除了類型爲any的。

類的繼承

派生類的構造函數必須包含「super」調用,而且訪問派生類的構造函數中的this以前,必須調用「super"

類修飾符

一、public: 全部人可見(默認)。

二、 private: 私有屬性

私有屬性只能在聲明的類中訪問,在子類或者生成的實例中都不能訪問,可是 private 屬性能夠在實例的方法中被訪問到,由於也至關於在類中訪問,可是子類的的實例方法確定是訪問不到的。

能夠把類的 constructor 定義爲私有類型,那麼這個類既不能被實例化也不能被繼承

三、 protected 受保護屬性

受保護屬性只能在聲明的類及其子類中訪問,可是 protected 屬性能夠在實例的方法中被訪問到,由於也至關於在類中訪問

能夠把類的 constructor 定義爲受保護類型,那麼這個類不能被實例化,可是能夠被繼承,至關於基類

四、 readonly 只讀屬性

只讀屬性必須具備初始值,或者在構造函數中初始化,初始化後就不能更改了,而且已經設置過初始值的只讀屬性,也是能夠在構造函數中被從新初始化的。可是在其子類的構造函數中不能被從新初始化。

五、 static 靜態屬性

只能經過類的名稱調用,不能在實例和構造函數或者子類中的構造函數和實例中訪問,可是靜態屬性是能夠繼承的,用子類的類名能夠訪問

注意:構造函數的參數也能夠添加修飾符,這樣能夠將參數直接定義爲類的屬性

class Dog {
  constructor(name: string) {
    this.name = name
    this.legs = 4 // 已經有默認值的只讀屬性是能夠被從新初始化的
  }
  public name: string
  run() { }
  private pri() { }
  protected pro() { }
  readonly legs: number = 3
  static food: string = 'bones'
}
let dog = new Dog('jinmao')
// dog.pri() // 私有屬性不能在實例中調用
// dog.pro() // 受保護的屬性,不能在實例中調用
console.log(Dog.food) // 'bones'


class Husky extends Dog {
  constructor(name: string, public color: string) {
    super(name)
    this.color = color
    // this.legs = 5 // 子類的構造函數中是不能對父類的只讀屬性從新初始化的
    // this.pri() // 子類不能調用父類的私有屬性
    this.pro() // 子類能夠調用父類的受保護屬性
  }
  protected age: number = 3
  private nickname: string = '二哈'
  info(): string {
    return this.age + this.nickname
  }
  // color: string // 參數用了修飾符,能夠直接定義爲屬性,這裏就不須要了
}

let husky = new Husky('husky', 'black')
husky.info() // 若是調用的類的方法中有對類的私有屬性和受保護屬性的訪問,這是不報錯的。
console.log(Husky.food) // 'bones' 子類能夠調用父類的靜態屬性

複製代碼

抽象類

只能被繼承,不能被實例化的類。

在抽象類中能夠添加共有的方法,也能夠添加抽象方法,而後由子類具體實現

abstract class Animal {
  eat() {
    console.log('eat')
  }
  abstract sleep(): void // 抽象方法,在子類中實現
}
// let animal = new Animal() // 會報錯,抽象類沒法建立實例


class Cat extends Animal {
  constructor(public name: string) {
    super()
  }
  run() { }
  // 必須實現抽象方法
  sleep() {
    console.log('sleep')
  }
}

let cat = new Cat('jiafei')
cat.eat()
複製代碼

接口類

  • 類實現接口時,必須實現接口的所有屬性,不過類能夠定義本身的屬性
  • 接口不能約束類的構造函數,只能約束公有成員
interface Human {
  // new (name:string):void // 接口不能約束類的構造函數
  name: string;
  eat(): void;
}

class Asian implements Human {
  constructor (name: string) {
    this.name = name
  }
  name: string
  // private name: string // 實現接口時用了私有屬性會報錯
  eat() {}
  sleep(){}
}

複製代碼

接口繼承類

至關於把類的成員抽象出來,只有類的成員結構,可是沒有具體實現

接口抽離類成員時不只抽離了公有屬性,還抽離了私有屬性和受保護屬性,因此非繼承的子類都會報錯

被抽象的類的子類,也能夠實現類抽象出來的接口,並且不用實現這個子類的父類已有的屬性

class Auto {
  state = 1
  // protected state2 = 0 // 下面的C會報錯,由於C並非 Auto 的子類,C只是實現了 Auto 抽象出來的接口
}
interface AutoInterface extends Auto {

}
class C implements AutoInterface {
  state = 1
}

// 被抽象的類的子類,也能夠實現類抽象出來的接口,並且不用實現父類的已有的屬性
class Bus extends Auto implements AutoInterface {
  // 不用設置 state ,Bus 的父類已經有了。

}

複製代碼

泛型

泛型函數

注意:用泛型定義函數類型時的位置不用,決定是否須要指定參數類型,見下面例子。

泛型函數例子

function log<T>(value: T): T {
  console.log(value)
  return value
}

log<string[]>(['a', 'b'])
log([1, 2]) // 能夠不用指定類型,TS會自動推斷

// 還能夠用類型別名定義泛型函數
//下面的定義不用指定參數類型
type Log = <T>(value:T) => T // 不用指定參數類型,會本身推斷
let myLog: Log = log
//下面的定義必須指定參數類型
type Log<T> = (value:T) => T // 若是這樣用泛型定義函數類型,必須指定一個參數類型
let myLog: Log<string> = log
複製代碼

泛型接口

function log<T>(value: T): T {
  console.log(value)
  return value
}

// 如下僅約束泛型接口中的一個泛型函數,實現不用指定泛型的參數類型
interface Log {
  <T>(value: T): T;
}
let myLog: Log = log

// 如下約束整個泛型接口,實現須要指定泛型的參數類型,或者用帶默認類型的泛型
interface Log1<T> {
  (value: T): T;
}
let myLog1: Log1<string> = log

interface Log2<T = string> {
  (value: T): T
}
let myLog2: Log2 = log
複製代碼

注意:泛型接口的泛型定義爲全局時,實現必須指定一個參數類型,或者用帶默認類型的泛型

泛型類

class Log3<T> {
  // 靜態成員不能引用類的泛型參數
  // static start(value: T) {
  // console.log(value)
  // }
  run(value: T) {
    console.log(value)
    return value
  }
}
let log3 = new Log3<number>()
log3.run(1)

//不指定類型,就能夠傳入任何類型
let log4 = new Log3()
log4.run('abc')
複製代碼

注意:泛型不能應用於類的靜態成員。而且實例化時,不指定類型,就能夠傳入任何類型

泛型約束

約束泛型傳入的類型

interface Length {
  length: number
}
function log5<T extends Length>(value: T) {
  // 想要打印出定義爲泛型T的value的length屬性,則T必需要有length屬性,因此須要泛型約束,T繼承length接口後,就確定具備了length屬性
  console.log(value,value.length)
  return value
}
log5([1])
log5('abc')
log5({length: 1})
複製代碼

泛型總結

  • 利用泛型,函數和類能夠輕鬆地支持多種類型,加強程序的擴展性
  • 沒必要寫多條函數重載,冗長的聯合類型聲明,加強代碼可讀性
  • 靈活控制類型之間的約束

類型檢查機制

類型檢查機制: TypeScript 編譯器在作類型檢查時,所秉承的一些原則,以及表現出的一些行爲。其做用是輔助開發,提升開發效率

類型推斷

類型推斷: 指的是不須要指定變量的類型(函數的返回值類型),TypeScript 能夠根據某些規則自動地爲其推斷出一個類型

基礎類型推斷

let a = 1 // 推斷爲 number
let b = [1] // 推斷爲 number[]
let c = (x = 1) => x + 1 // 推斷爲 (x?: number) => number
複製代碼

最佳通用類型推斷

當須要從多個類型中推斷出一個類型的時候,TypeScript 會盡量的推斷出一個兼容當前全部類型的通用類型

let d = [1, null]
// 推斷爲一個最兼容的類型,因此推斷爲(number | null)[]
// 當關閉"strictNullChecks"配置項時,null是number的子類型,因此推斷爲number[]
複製代碼

上下文類型推斷

以上的推斷都是從右向左,即根據表達式推斷,上下文類型推斷是從左向右,一般會發生在事件處理中。

類型斷言

在肯定本身比 TS 更準確的知道類型時,可使用類型斷言來繞過 TS 的檢查,改造舊代碼頗有效,可是防止濫用。

interface Bar {
  bar: number
}
let foo = {} as Bar
foo.bar = 1

// 可是推薦變量聲明時就要指定類型
let foo1: Bar = {
  bar: 1
}
複製代碼

類型兼容

當一個類型Y能夠被賦值給另外一個類型X時,咱們就能夠說類型X兼容類型Y

X兼容Y:X(目標類型) = Y(源類型)

let s: string = 'a'
s = null // 把編譯配置中的strictNullChecks設置成false,字符類型是兼容null類型的(由於null是字符的子類型)
複製代碼

接口兼容

成員少的兼容成員多的

interface X {
  a: any;
  b: any;
}
interface Y {
  a: any;
  b: any;
  c: any;
}

let x: X = { a: 1, b: 2 }
let y: Y = { a: 1, b: 2, c: 3 }
// 源類型只要具備目標類型的必要屬性,就能夠進行賦值。接口之間相互兼容,成員少的兼容成員多的。
x = y
// y = x // 不兼容
複製代碼

函數兼容性

type Handler = (a: number, b: number) => void
function test(handler: Handler) {
  return handler
}
複製代碼
一、參數個數
固定參數

目標函數的參數個數必定要多於源函數的參數個數

Handler 目標函數,傳入 test 的 參數函數 就是源函數

let handler1 = (a: number) => { }
test(handler1) // 傳入的函數能接收一個參數,且參數是number,是兼容的
let handler2 = (a: number, b: number, c: number) => { }
test(handler2) // 會報錯 傳入的函數能接收三個參數(參數多了),且參數是number,是不兼容的
複製代碼
可選參數和剩餘參數
let a1 = (p1: number, p2: number) => { }
let b1 = (p1?: number, p2?: number) => { }
let c1 = (...args: number[]) => { }
複製代碼

(1) 固定參數是能夠兼容可選參數和剩餘參數的

a1 = b1 // 兼容
a1 = c1 // 兼容
複製代碼

(2) 可選參數是不兼容固定參數和剩餘參數的,可是能夠經過設置"strictFunctionTypes": false來消除報錯,實現兼容

b1 = a1 //不兼容
b1 = c1 // 不兼容
複製代碼

(3) 剩餘參數能夠兼容固定參數和可選參數

c1 = a1 // 兼容
c1 = b1 // 兼容
複製代碼
二、參數類型
基礎類型
// 接上面的test函數
let handler3 = (a: string) => { }
test(handler3) // 類型不兼容
複製代碼
接口類型

接口成員多的兼容成員少的,也能夠理解把接口展開,參數多的兼容參數少的。對於不兼容的,也能夠經過設置"strictFunctionTypes": false來消除報錯,實現兼容

interface Point3D {
  x: number;
  y: number;
  z: number;
}
interface Point2D {
  x: number;
  y: number;
}
let p3d = (point: Point3D) => { }
let p2d = (point: Point2D) => { }

p3d = p2d // 兼容
p2d = p3d // 不兼容
複製代碼
三、返回值類型

目標函數的返回值類型必須與源函數的返回值類型相同,或者是其子類型

let f = () => ({ name: 'Alice' })
let g = () => ({ name: 'A', location: 'beijing' })
f = g // 兼容
g = f // 不兼容
複製代碼
四、函數重載

函數重載列表(目標函數)

function overload(a: number, b: number): number;
function overload(a: string, b: string): string;
複製代碼

函數的具體實現(源函數)

function overload(a: any, b: any): any { }
複製代碼

目標函數的參數要多於源函數的參數才能兼容

function overload(a:any,b:any,c:any):any {} // 具體實現時的參數多於重載列表中匹配到的第一個定義的函數的參數,也就是源函數的參數多於目標函數的參數,不兼容
複製代碼

返回值類型不兼容

function overload(a:any,b:any) {} // 去掉了返回值的any,不兼容
複製代碼

枚舉類型兼容性

enum Fruit { Apple, Banana }
enum Color { Red, Yello }
複製代碼
枚舉類型和數字類型是徹底兼容的
let fruit: Fruit.Apple = 4
let no: number = Fruit.Apple
複製代碼
枚舉類型之間是徹底不兼容的
let color: Color.Red = Fruit.Apple // 不兼容
複製代碼

類的兼容性

和接口比較類似,只比較結構,須要注意,在比較兩個類是否兼容時,靜態成員和構造函數是不參與比較的,若是兩個類具備相同的實例成員,那麼他們的實例就相互兼容

class A {
  constructor(p: number, q: number) { }
  id: number = 1
}
class B {
  static s = 1
  constructor(p: number) { }
  id: number = 2
}
let aa = new A(1, 2)
let bb = new B(1)
// 兩個實例徹底兼容,靜態成員和構造函數是不比較的
aa = bb
bb = aa
複製代碼
私有屬性

類中存在私有屬性狀況有兩種,若是其中一個類有私有屬性,另外一個沒有。沒有的能夠兼容有的,若是兩個類都有,那兩個類都不兼容。

若是一個類中有私有屬性,另外一個類繼承了這個類,那麼這兩個類就是兼容的。

class A {
  constructor(p: number, q: number) { }
  id: number = 1
  private name:string = '' // 只在A類中加這個私有屬性,aa不兼容bb,可是bb兼容aa,若是A、B兩個類中都加了私有屬性,那麼都不兼容
}
class B {
  static s = 1
  constructor(p: number) { }
  id: number = 2
}
let aa = new A(1, 2)
let bb = new B(1)
aa = bb // 不兼容
bb = aa // 兼容


// A中有私有屬性,C繼承A後,aa和cc是相互兼容的
class C extends A { }
let cc = new C(1, 2)
// 兩個類的實例是兼容的
aa = cc
cc = aa
複製代碼

泛型兼容

泛型接口

泛型接口爲空時,泛型指定不一樣的類型,也是兼容的。

interface Empty<T> {}

let obj1:Empty<number> = {}
let obj2:Empty<string> = {}
// 兼容
obj1 = obj2
obj2 = obj1
複製代碼

若是泛型接口中有一個接口成員時,類型不一樣就不兼容了

interface Empty<T> {
  value: T
}

let obj1:Empty<number> = {}
let obj2:Empty<string> = {}
// 報錯,都不兼容
obj1 = obj2
obj2 = obj1
複製代碼
泛型函數

兩個泛型函數若是定義相同,沒有指定類型參數的話也是相互兼容的

let log1 = <T>(x: T): T => {
  return x
}
let log2 = <U>(y: U): U => {
  return y
}
log1 = log2
log2 = log1
複製代碼

兼容性總結

  • 結構之間兼容:成員少的兼容成員多的
  • 函數之間兼容:參數多的兼容參數少的

類型保護機制

指的是 TypeScript 可以在特定的區塊(類型保護區塊)中保證變量屬於某種特定的類型。能夠在此區塊中放心地引用此類型的屬性,或者調用此類型的方法。

前置代碼,以後的代碼在此基礎運行

enum Type { Strong, Week }

class Java {
  helloJava() {
    console.log('hello Java')
  }
  java: any
}

class JavaScript {
  helloJavaScript() {
    console.log('hello JavaScript')
  }
  javaScript: any
}
複製代碼

實現 getLanguage 方法直接用 lang.helloJava 是否是存在做爲判斷是會報錯的

function getLanguage(type: Type, x: string | number) {
  let lang = type === Type.Strong ? new Java() : new JavaScript()

  // 若是想根據lang實例的類型,直接用lang.helloJava是否是存在來做爲判斷是會報錯的,由於如今lang是Java和JavaScript這兩種類型的聯合類型
  if (lang.helloJava) {
    lang.helloJava()
  } else {
    lang.helloJavaScript()
  }
  return lang
}
複製代碼

利用以前的知識可使用類型斷言解決

function getLanguage(type: Type, x: string | number) {
  let lang = type === Type.Strong ? new Java() : new JavaScript()

  // 這裏就須要用類型斷言來告訴TS當前lang實例要是什麼類型的
  if ((lang as Java).helloJava) {
    (lang as Java).helloJava()
  } else {
    (lang as JavaScript).helloJavaScript()
  }
  return lang
}
複製代碼

類型保護第一種方法,instanceof

function getLanguage(type: Type, x: string | number) {
  let lang = type === Type.Strong ? new Java() : new JavaScript()

  // instanceof 能夠判斷實例是屬於哪一個類,這樣TS就能判斷了。
  if (lang instanceof Java) {
    lang.helloJava()
  } else {
    lang.helloJavaScript()
  }
  return lang
}
複製代碼

類型保護第二種方法, in 能夠判斷某個屬性是否是屬於某個對象

function getLanguage(type: Type, x: string | number) {
  let lang = type === Type.Strong ? new Java() : new JavaScript()

  // in 能夠判斷某個屬性是否是屬於某個對象 如上helloJava和java都能判斷出來
  if ('java' in lang) {
    lang.helloJava()
  } else {
    lang.helloJavaScript()
  }
  return lang
}
複製代碼

類型保護第三種方法, typeof 類型保護,能夠幫助咱們判斷基本類型

function getLanguage(type: Type, x: string | number) {
  let lang = type === Type.Strong ? new Java() : new JavaScript()

  // x也是聯合類型,typeof類型保護,能夠判斷出基本類型。
  if (typeof x === 'string') {
    x.length
  } else {
    x.toFixed(2)
  }
  return lang
}
複製代碼

類型保護第四種方法,經過建立一個類型保護函數來判斷對象的類型

類型保護函數的返回值有點不一樣,用到了 is ,叫作類型謂詞

function isJava(lang: Java | JavaScript): lang is Java {
  return (lang as Java).helloJava !== undefined
}
複製代碼
function getLanguage(type: Type, x: string | number) {
  let lang = type === Type.Strong ? new Java() : new JavaScript()

  // 經過建立一個類型保護函數來判斷對象的類型
  if (isJava(lang)) {
    lang.helloJava()
  } else {
    lang.helloJavaScript()
  }
  return lang
}
複製代碼

總結

不一樣的判斷方法有不一樣的使用場景:

  • typeof:判斷一個變量的類型(多用於基本類型)
  • instanceof:判斷一個實例是否屬於某個類
  • in:判斷一個屬性是否屬於某個對象
  • 類型保護函數:某些判斷可能不是一條語句可以搞定的,須要更多複雜的邏輯,適合封裝到一個函數內

高級類型

交叉類型

& 符號。雖然叫交叉類型,可是是取的全部類型的並集

interface DogInterface {
  run(): void
}

interface CatInterface {
  jump(): void
}

// 交叉類型 用 & 符號。雖然叫交叉類型,可是是取的全部類型的並集。
let pet: DogInterface & CatInterface = {
  run() { },
  jump() { }
}
複製代碼

聯合類型

聲明的類型並不肯定,能夠爲多個類型中的一個,除了能夠是 TS 中規定的類型外,還有字符串字面量聯合類型、數字字面量聯合類型

let a: number | string = 1;

// 字符串字面量聯合類型
let b: 'a' | 'b' | 'c' = 'a'

// 數字字面量聯合類型
let c: 1 | 2 | 3 = 1
複製代碼

對象聯合類型

對象的聯合類型,只能取二者共有的屬性,因此說對象聯合類型只能訪問全部類型的交集

// 接上文DogInterface和CatInterface
class Dog implements DogInterface {
  run() { }
  eat() { }
}

class Cat implements CatInterface {
  jump() { }
  eat() { }
}
enum Master { Boy, Girl }
function getPet(master: Master) {
  // pet爲Dog和Cat的聯合類型,只能取二者共有的屬性,因此說聯合類型在此時只能訪問全部類型的交集
  let pet = master === Master.Boy ? new Dog() : new Cat()
  pet.eat()
  // pet.run() // 不能訪問,會報錯
  return pet
}
複製代碼

可區分的聯合類型

這種模式是結合了聯合類型和字面量類型的類型保護方法,一個類型若是是多個類型的聯合類型,而且每一個類型之間有一個公共的屬性,咱們就能夠憑藉這個公共屬性來建立不一樣的類型保護區塊。

核心是利用兩種或多種類型的共有屬性,來建立不一樣的代碼保護區塊

下面的函數若是隻有 Square 和 Rectangle 這兩種聯合類型,沒有問題,可是一旦擴展增長 Circle 類型,類型校驗就不會正常運行,並且也不報錯,這個時候咱們是但願代碼有報錯提醒的。

interface Square {
  kind: "square";
  size: number;
}
interface Rectangle {
  kind: 'rectangle';
  width: number;
  height: number;
}
interface Circle {
  kind: 'circle';
  r: number;
}
type Shape = Square | Rectangle | Circle

// 下面的函數若是隻有Square和Rectangle這兩種聯合類型,沒有問題,可是一旦擴展增長Circle類型,不會正常運行,並且也不報錯,這個時候咱們是但願代碼有報錯提醒的。
function area(s: Shape) {
  switch (s.kind) {
    case "square":
      return s.size * s.size;
      break;
    case "rectangle":
      return s.width * s.height;
      break;
  }
}

console.log(area({ kind: 'circle', r: 1 }))
// undefined,不報錯,這個時候咱們是但願代碼有報錯提醒的
複製代碼

若是想要獲得正確的報錯提醒,第一種方法是設置明確的返回值,第二種方法是利用 never 類型.

第一種方法是設置明確的返回值

// 會報錯:函數缺乏結束返回語句,返回類型不包括 "undefined"
function area(s: Shape): number {
  switch (s.kind) {
    case "square":
      return s.size * s.size;
      break;
    case "rectangle":
      return s.width * s.height;
      break;
  }
}
複製代碼

第二種方法是利用never類型,原理是在最後default判斷分支寫一個函數,設置參數是never類型,而後把最外面函數的參數傳進去,正常狀況下是不會執行到default分支的。

function area(s: Shape) {
  switch (s.kind) {
    case "square":
      return s.size * s.size;
      break;
    case "rectangle":
      return s.width * s.height;
      break;
    case "circle":
      return Math.PI * s.r ** 2;
      break;
    default:
      return ((e: never) => { throw new Error(e) })(s) //這個函數就是用來檢查s是不是never類型,若是snever類型,說明前面的分支所有覆蓋了,若是s不是never類型,說明前面的分支有遺漏,就得須要補一下。 } } 複製代碼

索引類型

索引類型的查詢操做符

keyof T 表示類型T的全部公共屬性的字面量的聯合類型

interface Obj {
  a: number;
  b: string;
}
let key: keyof Obj
// key的類型就是Obj的屬性a和b的聯合類型:let key: "a" | "b"
複製代碼

索引訪問操做符

T[K] 表示對象T的屬性K所表明的類型

interface Obj {
  a: number;
  b: string;
}
let value: Obj['a']
// value的類型就是Obj的屬性a的類型: let value: number
複製代碼

泛型約束

T extends U 泛型變量能夠繼承某個類型得到某些屬性

先看以下代碼片斷存在的問題。

let obj = {
  a: 1,
  b: 2,
  c: 3
}

//以下函數若是訪問obj中不存在的屬性也是沒有報錯的。
function getValues(obj: any, keys: string[]) {
  return keys.map(key => obj[key])
}

console.log(getValues(obj, ['a', 'b']))
console.log(getValues(obj, ['e', 'f']))
// 會顯示[undefined, undefined],可是TS編譯器並無報錯。
複製代碼

解決以下

function getValuest<T, K extends keyof T>(obj: T, keys: K[]): T[K][] {
  return keys.map(key => obj[key])
}
console.log(getValuest(obj, ['a', 'b']))
// console.log(getValuest(obj, ['e', 'f'])) // 這樣就會報錯了
複製代碼

映射類型

能夠從一箇舊的類型,生成一個新的類型

如下代碼用到了TS內置的映射類型

interface Obj {
  a: string;
  b: number;
  c: boolean
}

// 如下三種類型稱爲同態,只會做用於Obj的屬性,不會引入新的屬性
//把一個接口的全部屬性變成只讀
type ReadonlyObj = Readonly<Obj>
//把一個接口的全部屬性變成可選
type PartialObj = Partial<Obj>
//能夠抽取接口的子集
type PickObj = Pick<Obj, 'a' | 'b'>

// 非同態 會建立新的屬性
type RecordObj = Record<'x' | 'y', Obj>
// 建立一個新的類型並引入指定的新的類型爲
// {
// x: Obj;
// y: Obj;
// }
複製代碼

條件類型

T extends U ? X : Y

type TypeName<T> =
  T extends string ? "string" :
  T extends number ? "number" :
  T extends boolean ? "boolean" :
  T extends undefined ? "undefined" :
  T extends Function ? "function" :
  "object"

type T1 = TypeName<string> // 獲得的類型即: type T1 = "string"
type T2 = TypeName<string[]> // 獲得的類型即:type T2 = "object"
複製代碼

分佈式條件類型

(A | B) extends U ? X : Y 等價於 (A extends U ? X : Y) | (B extends U ? X : Y)

// 接上文
type T3 = TypeName<string | string[]> // 獲得的類型即:type T3 = "string" | "object"
複製代碼

用法一:利用分佈式條件類型能夠實現 Diff 操做

type T4 = Diff<"a" | "b" | "c", "a" | "e"> // 即:type T4 = "b" | "c"
// 拆分一下具體步驟
// Diff<"a","a" | "e"> | Diff<"b","a" | "e"> | Diff<"c", "a" | "e">
// 分佈結果以下:never | "b" | "c"
// 最終得到字面量的聯合類型 "b" | "c"
複製代碼

用法二:在Diff的基礎上實現過濾掉 null 和 undefined 的值。

type NotNull<T> = Diff<T, undefined | null>
type T5 = NotNull<string | number | undefined | null> // 即:type T5 = string | number
複製代碼

以上的類型別名在TS的類庫中都有內置的類型

  • Diff => Exclude<T, U>
  • NotNull => NonNullable<T>

此外,內置的還有不少類型,好比從類型T中抽取出能夠賦值給U的類型 Extract<T, U>

type T6 = Extract<"a" | "b" | "c", "a" | "e"> // 即:type T6 = "a"
複製代碼

好比: 用於提取函數類型的返回值類型 ReturnType<T>

先寫出 ReturnType<T> 的實現,infer 表示在 extends 條件語句中待推斷的類型變量。

type ReturnType<T extends (...args: any) => any> = T extends (...args: any) => infer R ? R : any;
複製代碼

分析一下上面的代碼,首先要求傳入 ReturnType 的 T 必須能賦值給一個最寬泛的函數,以後判斷 T 能不能賦值給一個能夠接受任意參數的返回值待推斷爲 R 的函數,若是能夠,返回待推斷返回值 R ,若是不能夠,返回 any 。

type T7 = ReturnType<() => string> //即:type T7 = string
複製代碼
相關文章
相關標籤/搜索