詳解TS對象擴展運算符和rest運算符

概述

TypeScript 2.1 增加瞭對 對象擴展運算和 rest 屬性提案的支持,該提案在 ES2018 中標準化。可以以類型安全的方式使用 rest 和 spread 屬性。

對象 rest 屬性

假設已經定義瞭一個具有三個屬性的簡單字面量對象

const marius = {
  name: "Marius Schulz",
  website: "https://mariusschulz.com/",
  twitterHandle: "@mariusschulz"
};

使用 ES6 解構語法,可以創建幾個局部變量來保存相應屬性的值。TypeScript 將正確地推斷每個變量的類型:

const { name, website, twitterHandle } = marius;

name;          // Type string
website;       // Type string
twitterHandle; // Type string

這些都是正確的,但這到現在也啥新鮮的。除瞭提取感興趣的一組屬性之外,還可以使用…語法將所有剩餘的屬性收集到rest元素中:

const { twitterHandle, ...rest } = marius;

twitterHandle; // Type string
rest; // Type { name: string; website: string; }

TypeScript 會為得到結果的局部變量確定正確的類型。雖然twitterHandle變量是一個普通的字符串,但rest變量是一個對象,其中包含剩餘兩個未被解構的屬性。

對象擴展屬性

假設咱們希望使用fetch()API 發出 HTTP 請求。它接受兩個參數:一個URL和一個options對象,options包含請求的任何自定義設置。

在應用程序中,可以封裝對fetch()的調用,並提供默認選項和覆蓋給定請求的特定設置。這些配置項類似如下:

const defaultOptions = {
  method: "GET",
  credentials: "same-origin"
};

const requestOptions = {
  method: "POST",
  redirect: "follow"
};

使用對象擴展,可以將兩個對象合並成一個新對象,然後傳遞給fetch()方法

// Type { method: string; redirect: string; credentials: string; }
const options = {
  ...defaultOptions,
  ...requestOptions
};

對象擴展屬性創建一個新對象,復制defaultOptions中的所有屬性值,然後按照從左到右的順序復制requestOptions中的所有屬性值,最後得到的結果如下:

console.log(options);
// {
//   method: "POST",
//   credentials: "same-origin",
//   redirect: "follow"
// }

請註意,分配順序很重要。如果一個屬性同時出現在兩個對象中,則後分配的會替換前面的。

當然,TypeScript 理解這種順序。因此,如果多個擴展對象使用相同的鍵定義一個屬性,那麼結果對象中該屬性的類型將是最後一次賦值的屬性類型,因為它覆蓋瞭先前賦值的屬性:

const obj1 = { prop: 42 };
const obj2 = { prop: "Hello World" };

const result1 = { ...obj1, ...obj2 }; // Type { prop: string }
const result2 = { ...obj2, ...obj1 }; // Type { prop: number }

制作對象的淺拷貝

對象擴展可用於創建對象的淺拷貝。假設咱希望通過創建一個新對象並復制所有屬性來從現有todo項創建一個新todo項,使用對象就可以輕松做到:

const todo = {
  text: "Water the flowers",
  completed: false,
  tags: ["garden"]
};

const shallowCopy = { ...todo };

實際上,你會得到一個新對象,所有的屬性值都被復制:

console.log(todo === shallowCopy);
// false

console.log(shallowCopy);
// {
//   text: "Water the flowers",
//   completed: false,
//   tags: ["garden"]
// }

現在可以修改text屬性,但不會修改原始的todo項:

hallowCopy.text = "Mow the lawn";

console.log(shallowCopy);
// {
//   text: "Mow the lawn",
//   completed: false,
//   tags: ["garden"]
// }

console.log(todo);
// {
//   text: "Water the flowers",
//   completed: false,
//   tags: ["garden"]
// }

但是,新的todo項引用與第一個相同的tags數組。由於是淺拷貝,改變數組將影響這兩個todo

shallowCopy.tags.push("weekend");

console.log(shallowCopy);
// {
//   text: "Mow the lawn",
//   completed: false,
//   tags: ["garden", "weekend"]
// }

console.log(todo);
// {
//   text: "Water the flowers",
//   completed: false,
//   tags: ["garden", "weekend"]
// }

如果想創建一個序列化對象的深拷貝,可以考慮使用jsON.parse(jsON.stringify(obj))或其他方法,如object.assign()。對象擴展僅拷貝屬性值,如果一個值是對另一個對象的引用,則可能導致意外的行為。

keyof 和查找類型

JS 是一種高度動態的語言。在靜態類型系統中捕獲某些操作的語義有時會很棘手。以一個簡單的prop函數為例:

function prop(obj, key) {
  return obj[key];
}

它接受一個對象和一個鍵,並返回相應屬性的值。一個對象的不同屬性可以有完全不同的類型,咱們甚至不知道obj是什麼樣子的。

那麼如何在 TypeScript 中編寫這個函數呢?先嘗試一下:

有瞭這兩個類型註釋,obj必須是對象,key必須是字符串。咱們現在已經限制瞭兩個參數的可能值集。然而,TS 仍然推斷返回類型為any:

const todo = {
  id: 1,
  text: "Buy milk",
  due: new Date(2016, 11, 31)
};

const id = prop(todo, "id");     // any
const text = prop(todo, "text"); // any
const due = prop(todo, "due");   // any

如果沒有更進一步的信息,TypeScript 就不知道將為key參數傳遞哪個值,所以它不能推斷出prop函數的更具體的返回類型。咱們需要提供更多的類型信息來實現這一點。

keyof 操作符號

在 JS 中屬性名稱作為參數的 API 是相當普遍的,但是到目前為止還沒有表達在那些 API 中出現的類型關系。

TypeScript 2.1 新增加keyof操作符。輸入索引類型查詢或keyof,索引類型查詢keyof T產生的類型是T的屬性名稱。假設咱們已經定義瞭以下Todo接口:

interface Todo {
  id: number;
  text: string;
  due: Date;
}

各位可以將keyof操作符應用於Todo類型,以獲得其所有屬性鍵的類型,該類型是字符串字面量類型的聯合

type TodoKeys = keyof Todo; // "id" | "text" | "due"

當然,各位也可以手動寫出聯合類型”id” | “text” | “due”,而不是使用keyof,但是這樣做很麻煩,容易出錯,而且維護起來很麻煩。而且,它應該是特定於Todo類型的解決方案,而不是通用的解決方案。

索引類型查詢

有瞭keyof,咱們現在可以改進prop函數的類型註解。我們不再希望接受任意字符串作為key參數。相反,咱們要求參數key實際存在於傳入的對象的類型上

function prop <T, K extends keyof T>(obj: T, key: K) {
  return obj[key]
}

TypeScript 現在以推斷prop函數的返回類型為T[K],這個就是所謂的索引類型查詢或查找類型。它表示類型T的屬性K的類型。如果現在通過prop方法訪問下面todo的三個屬性,那麼每個屬性都有正確的類型:

const todo = {
  id: 1,
  text: "Buy milk",
  due: new Date(2016, 11, 31)
};

const id = prop(todo, "id");     // number
const text = prop(todo, "text"); // string
const due = prop(todo, "due");   // Date

現在,如果傳遞一個todo對象上不存在的鍵會發生什麼

編譯器會報錯,這很好,它阻止咱們試圖讀取一個不存在的屬性。

另一個真實的示例,請查看與TypeScript編譯器一起發佈的lib.es2017.object.d.ts類型聲明文件中Object.entries()方法:

interface ObjectConstructor {
  // ...
  entries<T extends { [key: string]: any }, K extends keyof T>(o: T): [keyof T, T[K]][];
  // ...
}

entries方法返回一個元組數組,每個元組包含一個屬性鍵和相應的值。不可否認,在返回類型中有大量的方括號,但是我們一直在尋找類型安全性。

以上就是詳解TS對象擴展運算符和rest運算符的詳細內容,更多關於TS對象擴展運算符和rest運算符的資料請關註WalkonNet其它相關文章!

推薦閱讀: