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


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

interface DogInterface {
  jump(): void
}

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

 

2、聯合類型
 
聲明的類型並不確定,可以為多個類型中的一個。用豎線(|)分隔每個類型,所以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 函數中設定類型保護區塊,會發生什么呢?

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。

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

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

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

 

3、索引類型

使用索引類型,編譯器就能夠檢查使用了動態屬性名的代碼。例如:從js對象中選取屬性的子集,然后建立一個集合

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 T

對於任何類型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 函數
// 1、用T來約束obj
// 2、用K來約束keys數組
// 3、給K增加一個類型約束,讓它繼承obj的所有屬性的聯合類型
// 4、函數的返回值是一個數組,數組的元素的類型就是屬性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"”

 

4、映射類型

通過映射類型,可以從一個舊的類型生成一個新的類型,比如把一個類型中的所有屬性變成只讀
 
interface Obj {
  a: string;
  b: number;
  c: boolean;
}

4-1、同態

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

(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-2、非同態,會創建一些新的屬性

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

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

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

映射類型的本質是一些預定義的泛型接口,通常還會結合索引類型來獲取對象的屬性和屬性值,從而將一個對象映射成想要的結構


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM