在 TS 中如何實現類型保護?類型謂詞瞭解一下

1、聯合類型

在 TypeScript 中,一個變量不會被限制爲單一的類型。若是你但願一個變量的值,能夠有多種類型,那麼就可使用 TypeScript 提供的聯合類型。下面咱們來舉一個聯合類型的例子:typescript

let stringOrBoolean: string | boolean = "Semlinker";

interface Cat {
  numberOfLives: number;
}

interface Dog {
  isAGoodBoy: boolean;
}

let animal: Cat | Dog;

當咱們使用聯合類型時,咱們必須儘可能把當前值的類型收窄爲當前值的實際類型,而類型保護就是實現類型收窄的一種手段。express

昨晚新建了一個 「重學TypeScript」的微信羣,目前「重學TypeScript」專題已有文章十七篇(基礎、高級、設計模式等),計劃至少三十篇。想加羣的小夥伴,加我我的微信 semlinker,備註重學TS。

2、類型保護

A type guard is some expression that performs a runtime check that guarantees the type in some scope. —— TypeScript 官方文檔

類型保護是可執行運行時檢查的一種表達式,用於確保該類型在必定的範圍內。換句話說,類型保護能夠保證一個字符串是一個字符串,儘管它的值也能夠是一個數值。類型保護與特性檢測並非徹底不一樣,其主要思想是嘗試檢測屬性、方法或原型,以肯定如何處理值。目前主要有四種的方式來實現類型保護:編程

1. in 關鍵字segmentfault

interface Admin {
  name: string;
  privileges: string[];
}

interface Employee {
  name: string;
  startDate: Date;
}

type UnknownEmployee = Employee | Admin;

function printEmployeeInformation(emp: UnknownEmployee) {
  console.log("Name: " + emp.name);
  if ("privileges" in emp) {
    console.log("Privileges: " + emp.privileges);
  }
  if ("startDate" in emp) {
    console.log("Start Date: " + emp.startDate);
  }
}

2. typeof 關鍵字設計模式

function padLeft(value: string, padding: string | number) {
  if (typeof padding === "number") {
      return Array(padding + 1).join(" ") + value;
  }
  if (typeof padding === "string") {
      return padding + value;
  }
  throw new Error(`Expected string or number, got '${padding}'.`);
}

typeof 類型保護只支持兩種形式:typeof v === "typename"typeof v !== typename"typename" 必須是 "number""string""boolean""symbol"。 可是 TypeScript 並不會阻止你與其它字符串比較,語言不會把那些表達式識別爲類型保護。安全

3. instanceof 關鍵字微信

interface Padder {
  getPaddingString(): string;
}

class SpaceRepeatingPadder implements Padder {
  constructor(private numSpaces: number) {}
  getPaddingString() {
    return Array(this.numSpaces + 1).join(" ");
  }
}

class StringPadder implements Padder {
  constructor(private value: string) {}
  getPaddingString() {
    return this.value;
  }
}

let padder: Padder = new SpaceRepeatingPadder(6);

if (padder instanceof SpaceRepeatingPadder) {
  // padder的類型收窄爲 'SpaceRepeatingPadder'
}

4. 自定義類型保護的類型謂詞(type predicate)函數式編程

function isNumber(x: any): x is number {
  return typeof x === "number";
}

function isString(x: any): x is string {
  return typeof x === "string";
}

相信除了類型謂詞外,你們對其它三種方式都很熟悉了,下面咱們來着重介紹一下類型謂詞。函數

3、類型謂詞

在開始介紹類型謂詞前,咱們先來看一個示例:this

interface Vehicle {
  move: (distance: number) => void;
}

class Car implements Vehicle {
  move = (distance: number) => {
    // Move car…
  };

  turnSteeringWheel = (direction: string) => {
    // Turn wheel…
  };
}

class VehicleController {
  vehicle: Vehicle;
  constructor(vehicle: Vehicle) {
    this.vehicle = vehicle;
  }
}

const car = new Car();
const vehicleController = new VehicleController(car);

const { vehicle } = vehicleController;
vehicle.turnSteeringWheel('left');

儘管你知道汽車是一輛車,但 VehicleController 已經把它簡化爲一輛基本的汽車。由於 Vehicle 並無 turnSteeringWheel 屬性,因此對於以上代碼,TypeScript 編譯器會提示如下錯誤信息:

Property 'turnSteeringWheel' does not exist on type 'Vehicle'.

對於這個問題,咱們能夠利用 instanceof 關鍵字來確保當前的對象是 Car 汽車類的實例:

if(vehicle instanceof Car) {
   vehicle.turnSteeringWheel('left');
}

但該方案有必定的限制,即它只對類有效。當判斷的對象不是某個類的實例時就無效了,好比:

const anotherCar = {
  move: (distance: number) => null,
  turnSteeringWheel: (direction: string) => null
}; 

const anotherVehicleController = new VehicleController(anotherCar);
const { vehicle } = anotherVehicleController;

if (vehicle instanceof Car) {
  vehicle.turnSteeringWheel('left');
  console.log("這是一輛車");
} else {
  console.log("這不是一輛車");
}

儘管 anotherCar 跟前面已經定義的 car 擁有相同的形狀,但它並非 Car 汽車類的實例,所以在這種狀況下,vehicle instanceof Car 表達式返回的結果爲 false。因此以上代碼的輸出結果是:"這不是一輛車"。

儘管 typeofinstanceof 這兩個關鍵字在不少狀況下能夠知足類型保護的需求,但在函數式編程的領域它們的功能就受限了。那麼咱們應該如何檢查任何對象的類型的?幸運的是,你能夠建立自定義類型保護。

3.1 自定義類型保護

下面咱們繼續以車輛和汽車的例子爲例,來建立一個自定義類型保護函數 —— isCar,它的具體實現以下:

function isCar(vehicle: any): vehicle is Car {
  return (vehicle as Car).turnSteeringWheel !== undefined;
}

你能夠傳遞任何值給 isCar 函數,用來判斷它是否是一輛車。isCar 函數與普通函數的最大區別是,該函數的返回類型是 vehicle is Car,這就是咱們前面所說的 "類型謂詞"。

在 isCar 函數的方法體中,咱們不只要檢查 vehicle 變量是否含有 turnSteeringWheel 屬性,並且還要告訴 TS 編譯器,若是上述邏輯語句的返回結果是 true,那麼當前判斷的 vehicle 變量值的類型是 Car 類型。

如今讓咱們來重構一下前面的條件語句:

// vehicle instanceof Car -> isCar(anotherCar)
if (isCar(anotherCar)) {
  anotherCar.turnSteeringWheel('left');
  console.log("這是一輛車");
} else {
  console.log("這不是一輛車");
}

在重構完成後,咱們再次運行代碼,這時控制檯會輸出 "這是一輛車"。好了,如今問題已經解決了。接下來讓咱們來總結一下自定義類型保護有什麼用?

3.2 自定義類型保護有什麼用

自定義類型保護的主要特色是:

  • 返回類型謂詞,如 vehicle is Car
  • 包含能夠準確肯定給定變量類型的邏輯語句,如 (vehicle as Car).turnSteeringWheel !== undefined

對於基本數據類型來講,咱們也能夠自定義類型保護來保證類型安全,好比:

const isNumber = (variableToCheck: any): variableToCheck is number =>
  (variableToCheck as number).toExponential !== undefined;

const isString = (variableToCheck: any): variableToCheck is string =>
  (variableToCheck as string).toLowerCase !== undefined;

若是你要檢查的類型不少,那麼爲每種類型建立和維護惟一的類型保護可能會變得很繁瑣。針對這個問題,咱們能夠利用 TypeScript 的另外一個特性 —— 泛型,來解決複用問題:

function isOfType<T>(
  varToBeChecked: any,
  propertyToCheckFor: keyof T
): varToBeChecked is T {
  return (varToBeChecked as T)[propertyToCheckFor] !== undefined;
}

在以上代碼中,咱們定義了一個通用的類型保護函數,你能夠在須要的時候使用它來縮窄類型。之前面自定義類型保護的例子來講,咱們就能夠按照如下方式來使用 isOfType 通用的類型保護函數:

// isCar(anotherCar) -> isOfType<Car>(vehicle, 'turnSteeringWheel')
if (isOfType<Car>(vehicle, 'turnSteeringWheel')) {
  anotherCar.turnSteeringWheel('left');
  console.log("這是一輛車");
} else {
  console.log("這不是一輛車");
}

有了 isOfType 通用的類型保護函數以後,你沒必要再爲每一個要檢查的類型編寫惟一的類型保護函數。並且在實際的開發過程當中,只要咱們合理的使用類型保護函數,就可讓咱們的代碼在運行時可以保證類型安全。

4、參考資源

本人的全棧修仙之路訂閱號,會按期分享 Angular、TypeScript、Node.js/Java 、Spring 相關文章,歡迎感興趣的小夥伴訂閱哈!

full-stack-logo

相關文章
相關標籤/搜索