Typescript 實戰 --- (8)高級類型

一、交叉類型
 
將多個類型合併成一個類型,新的類型將具備全部類型的特性,適用於對象混用
 
語法:
類型1 & 類型2 & 類型3
interface CatInterface {
  run(): void
}

interface DogInterface {
  jump(): void
}

// 交叉類型具備全部類型的特性
let pet: CatInterface & DogInterface = {
  run() {},
  jump() {}
}

 

二、聯合類型
 
聲明的類型並不肯定,能夠爲多個類型中的一個。用豎線(|)分隔每一個類型,因此number | string | boolean表示一個值能夠是number,string,或boolean
let a: number | string = 2;
a = 'hello';
a = undefined; // 能夠爲其子類型

a = true;  // Error: 不能將類型「true」分配給類型「string | number」

 

(1)、字面量聯合類型:不只限制類型,還限制取值
// 字符串聯合類型
let x: 'typescript' | 'webpack' | 'nodejs';

x = 'webpack';
x = 'hello';   // Error: 不能將類型「"hello"」分配給類型「"typescript" | "webpack" | "nodejs"」


// 數字聯合類型
let y: 1 | 2 | 3;

y = 3;
y = 33;       // Error: 不能將類型「33」分配給類型「1 | 2 | 3」


let z: 'typescript' | 2;

z = 'typescript';
z = 2;
z = 1;       // Error: 不能將類型「1」分配給類型「"typescript" | 2」

 

(2)、對象聯合類型:在類型未肯定的狀況下,只能訪問全部類型的公用成員
enum Pet { Dog, Cat };

interface DogInterface {
  run(): void;
  eat(): void;
}

interface CatInterface {
  jump(): void;
  eat(): void;
}

class Dog implements DogInterface {
  run() {};
  eat() {};
}

class Cat implements CatInterface {
  jump() {};
  eat() {};
}

function getPet(pet: Pet) {
  // let smallPet: Dog | Cat
  let smallPet = pet === Pet.Dog ? new Dog() : new Cat();

  // 類型不肯定時,只能取公有成員
  smallPet.eat();

  smallPet.run();   // 類型「Dog | Cat」上不存在屬性「run」
  smallPet.jump();  // 類型「Dog | Cat」上不存在屬性「jump」

  return smallPet;
}

 

(3)、可區分的聯合類型:這種模式從本質上來說是結合了聯合類型和字面量聯合類型的一種類型保護方法
 
其核心思想是:若是一個類型是多個類型的聯合類型,而且每一個類型之間有一個公共的屬性,那麼就能夠利用這個公共的屬性建立不一樣的類型保護區塊
// 例如:Shape是多個類型的聯合類型,每一個類型都具備一個公共屬性kind,由此在 switch中創建了不一樣類型的保護區塊

interface Rectangle {
  kind: 'rectangle';
  width: number;
  height: number;
}

interface Square {
  kind: 'square';
  size: number;
}

type Shape = Rectangle | Square;

function area(s: Shape) {
  switch(s.kind) {
    case 'rectangle':
      return s.width * s.height;
    case 'square':
      return s.size * s.size;
  }
}

若是又添加了一個聯合類型,可是又沒有在 area 函數中設定類型保護區塊,會發生什麼呢?node

interface Rectangle {
  kind: 'rectangle';
  width: number;
  height: number;
}

interface Square {
  kind: 'square';
  size: number;
}

// 添加新的聯合類型
interface Circle {
  kind: 'circle';
  r: number;
}

type Shape = Rectangle | Square | Circle;

function area(s: Shape) {
  switch(s.kind) {
    case 'rectangle':
      return s.width * s.height;
    case 'square':
      return s.size * s.size;
  }
}

console.log(area({ kind: 'circle', r: 1 }));   // undefined

執行程序打印出了一個結果 undefined,因爲上例中並無在 area 方法中爲 Circle 指定計算面積的方法,理論上應該提示錯誤,而不是直接返回 undefined。webpack

爲了讓編譯器正確的提示錯誤,有兩種可選方法:web

(1)、爲 area 方法指定返回值類型typescript

function area(s: Shape): number {
  switch(s.kind) {
    case 'rectangle':
      return s.width * s.height;
    case 'square':
      return s.size * s.size;
  }
}

(2)、利用never類型數組

// 給定一個 default 分支,經過判斷 s 是否是 never 類型來提示錯誤。
// 若是是 never 類型,則能夠在前面的分支中找到對應的執行代碼;
// 若是不是 never 類型,則說明前面的代碼有遺漏,須要補全

function area(s: Shape): number {
  switch(s.kind) {
    case 'rectangle':
      return s.width * s.height;
    case 'square':
      return s.size * s.size;
    default:
      return ((e: never) => { throw new Error(e) })(s);
      // 類型「Circle」的參數不能賦給類型「never」的參數
  }
}

經過錯誤提示補全代碼函數

function area(s: Shape): number {
  switch(s.kind) {
    case 'rectangle':
      return s.width * s.height;
    case 'square':
      return s.size * s.size;
    case 'circle':
      return Math.PI * s.r ** 2
    default:
      return ((e: never) => { throw new Error(e) })(s);
  }
}

console.log(area({ kind: 'circle', r: 1 }));   // 3.141592653589793

 

三、索引類型spa

使用索引類型,編譯器就可以檢查使用了動態屬性名的代碼。例如:從js對象中選取屬性的子集,而後創建一個集合code

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

function getValues(obj: any, keys: string[]) {
  return keys.map(key => obj[key])
}

// obj 中存在的屬性
console.log(getValues(obj, ['a', 'b']));   // [ 1, 2 ]

// obj 中不存的屬性,返回 undefined,而沒有提示報錯
console.log(getValues(obj, ['e', 'f']));   // [ undefined, undefined ]

索引類型能夠用來解決上例中的問題,在認識索引類型以前須要先了解幾個概念:對象

(1)、索引類型查詢操做符   keyof Tblog

對於任何類型T,keyof T 的結果是 類型T的全部公共屬性的字面量的聯合類型

interface Person {
  name: string;
  gender: string;
  age: number;
}

let personProps: keyof Person;  // 'name' | 'gender' | 'age'
console.log(personProps)

 

(2)、索引訪問操做符     T[K]

類型T的屬性K所表明的類型
interface Person {
  name: string;
  gender: string;
  age: number;
}

let n: Person['name'];   // n 的類型是 string
let a: Person['age'];    // a 的類型是 number

 

(3)、泛型約束       T extends U
表示泛型變量能夠繼承某個類型得到某些屬性
 
結合以上三點來改造 getValues 函數
// 一、用T來約束obj
// 二、用K來約束keys數組
// 三、給K增長一個類型約束,讓它繼承obj的全部屬性的聯合類型
// 四、函數的返回值是一個數組,數組的元素的類型就是屬性K對應的類型

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

function getValues<T, K extends keyof T>(obj: T, keys: K[]): T[K][] {
  return keys.map(key => obj[key])
}

// obj 中存在的屬性
console.log(getValues(obj, ['a', 'b']));   // [ 1, 2 ]

console.log(getValues(obj, ['e', 'f']));   
// Error:不能將類型「string」分配給類型「"a" | "b" | "c"」

 

四、映射類型

經過映射類型,能夠從一箇舊的類型生成一個新的類型,好比把一個類型中的全部屬性變成只讀
 
interface Obj {
  a: string;
  b: number;
  c: boolean;
}

4-一、同態

同態的意思是:只會做用於舊類型的屬性,而不會引入新的屬性

(1)、Readonly<T>   將舊類型中的每個成員都變成只讀

type ReadonlyObj = Readonly<Obj>;

(2)、Partial<T>   把舊類型中的每個成員都變成可選的

type PartialObj = Partial<Obj>;

(3)、Pick<T, key1 | key2 | keyn>   能夠抽取舊類型中的一些子集

接受兩個參數:第一個是要抽取的對象,第二個是要抽取的屬性的key

type PickObj = Pick<Obj, 'a' | 'c'>;

 

4-二、非同態,會建立一些新的屬性

(1)、Record<key1 | key2 | keyn, T>   

接受兩個參數:第一個參數是一些預約義的新的屬性,第二個參數是一個已知的對象

type RecordObj = Record<'x' | 'y', Obj>;

映射類型的本質是一些預約義的泛型接口,一般還會結合索引類型來獲取對象的屬性和屬性值,從而將一個對象映射成想要的結構
相關文章
相關標籤/搜索