TypeScript 之常見型別(上)

冴羽發表於2021-12-08

TypeScript 的官方文件早已更新,但我能找到的中文文件都還停留在比較老的版本。所以對其中新增以及修訂較多的一些章節進行了翻譯整理。

本篇翻譯整理自 TypeScript Handbook 中 「Everyday Types」 章節。

本文並不嚴格按照原文翻譯,對部分內容也做了解釋補充。

常見型別(Everyday Types)

本章我們會講解 JavaScript 中最常見的一些型別,以及對應的描述方式。注意本章內容並不詳盡,後續的章節會講解更多命名和使用型別的方式。

型別可以出現在很多地方,不僅僅是在型別註解 (type annotations)中。我們不僅要學習型別本身,也要學習在什麼地方使用這些型別產生新的結構。

我們先複習下最基本和常見的型別,這些是構建更復雜型別的基礎。

原始型別: stringnumberboolean(The primitives)

JavaScript 有三個非常常用的原始型別stringnumberboolean,每一個型別在 TypeScript 中都有對應的型別。他們的名字跟你在 JavaScript 中使用 typeof 操作符得到的結果是一樣的。

  • string 表示字串,比如 "Hello, world"
  • number 表示數字,比如 42,JavaScript 中沒有 int 或者 float,所有的數字,型別都是 number
  • boolean 表示布林值,其實也就兩個值: truefalse

    型別名 StringNumberBoolean (首字母大寫)也是合法的,但它們是一些非常少見的特殊內建型別。所以型別總是使用 stringnumber 或者 boolean

陣列(Array)

宣告一個類似於 [1, 2, 3] 的陣列型別,你需要用到語法 number[]。這個語法可以適用於任何型別(舉個例子,string[] 表示一個字串陣列)。你也可能看到這種寫法 Array<number>,是一樣的。我們會在泛型章節為大家介紹 T<U> 語法。

注意 [number]number[] 表示不同的意思,參考元組章節

any

TypeScript 有一個特殊的型別,any,當你不希望一個值導致型別檢查錯誤的時候,就可以設定為 any

當一個值是 any 型別的時候,你可以獲取它的任意屬性 (也會被轉為 any 型別),或者像函式一樣呼叫它,把它賦值給一個任意型別的值,或者把任意型別的值賦值給它,再或者是其他語法正確的操作,都可以:

let obj: any = { x: 0 };
// None of the following lines of code will throw compiler errors.
// Using `any` disables all further type checking, and it is assumed 
// you know the environment better than TypeScript.
obj.foo();
obj();
obj.bar = 100;
obj = "hello";
const n: number = obj;

當你不想寫一個長長的型別程式碼,僅僅想讓 TypeScript 知道某段特定的程式碼是沒有問題的,any 型別是很有用的。

noImplicitAny

如果你沒有指定一個型別,TypeScript 也不能從上下文推斷出它的型別,編譯器就會預設設定為 any 型別。

如果你總是想避免這種情況,畢竟 TypeScript 對 any 不做型別檢查,你可以開啟編譯項 noImplicitAny,當被隱式推斷為 any 時,TypeScript 就會報錯。

變數上的型別註解(Type Annotations on Variables)

當你使用 constvarlet 宣告一個變數時,你可以選擇性的新增一個型別註解,顯式指定變數的型別:

let myName: string = "Alice";
TypeScript 並不使用“在左邊進行型別宣告”的形式,比如 int x = 0;型別註解往往跟在要被宣告型別的內容後面。

不過大部分時候,這不是必須的。因為 TypeScript 會自動推斷型別。舉個例子,變數的型別可以基於初始值進行推斷:

// No type annotation needed -- 'myName' inferred as type 'string'
let myName = "Alice";

大部分時候,你不需要學習推斷的規則。如果你剛開始使用,嘗試儘可能少的使用型別註解。你也許會驚訝於,TypeScript 僅僅需要很少的內容就可以完全理解將要發生的事情。

函式(Function)

函式是 JavaScript 傳遞資料的主要方法。TypeScript 允許你指定函式的輸入值和輸出值的型別。

引數型別註解(Parameter Type Annotations)

當你宣告一個函式的時候,你可以在每個引數後面新增一個型別註解,宣告函式可以接受什麼型別的引數。引數型別註解跟在引數名字後面:

// Parameter type annotation
function greet(name: string) {
  console.log("Hello, " + name.toUpperCase() + "!!");
}

當引數有了型別註解的時候,TypeScript 便會檢查函式的實參:

// Would be a runtime error if executed!
greet(42);
// Argument of type 'number' is not assignable to parameter of type 'string'.
即便你對引數沒有做型別註解,TypeScript 依然會檢查傳入引數的數量是否正確

返回值型別註解(Return Type Annotations)

你也可以新增返回值的型別註解。返回值的型別註解跟在引數列表後面:

function getFavoriteNumber(): number {
  return 26;
}

跟變數型別註解一樣,你也不需要總是新增返回值型別註解,TypeScript 會基於它的 return 語句推斷函式的返回型別。像這個例子中,型別註解寫和沒寫都是一樣的,但一些程式碼庫會顯式指定返回值的型別,可能是因為需要編寫文件,或者阻止意外修改,亦或者僅僅是個人喜好。

匿名函式(Anonymous Functions)

匿名函式有一點不同於函式宣告,當 TypeScript 知道一個匿名函式將被怎樣呼叫的時候,匿名函式的引數會被自動的指定型別。

這是一個例子:

// No type annotations here, but TypeScript can spot the bug
const names = ["Alice", "Bob", "Eve"];
 
// Contextual typing for function
names.forEach(function (s) {
  console.log(s.toUppercase());
  // Property 'toUppercase' does not exist on type 'string'. Did you mean 'toUpperCase'?
});
 
// Contextual typing also applies to arrow functions
names.forEach((s) => {
  console.log(s.toUppercase());
  // Property 'toUppercase' does not exist on type 'string'. Did you mean 'toUpperCase'?
});

儘管引數 s 並沒有新增型別註解,但 TypeScript 根據 forEach 函式的型別,以及傳入的陣列的型別,最後推斷出了 s 的型別。

這個過程被稱為上下文推斷(contextual typing),因為正是從函式出現的上下文中推斷出了它應該有的型別。

跟推斷規則一樣,你也不需要學習它是如何發生的,只要知道,它確實存在並幫助你省掉某些並不需要的註解。後面,我們還會看到更多這樣的例子,瞭解一個值出現的上下文是如何影響它的型別的。

物件型別(Object Types)

除了原始型別,最常見的型別就是物件型別了。定義一個物件型別,我們只需要簡單的列出它的屬性和對應的型別。

舉個例子:

// The parameter's type annotation is an object type
function printCoord(pt: { x: number; y: number }) {
  console.log("The coordinate's x value is " + pt.x);
  console.log("The coordinate's y value is " + pt.y);
}
printCoord({ x: 3, y: 7 });

這裡,我們給引數新增了一個型別,該型別有兩個屬性, xy,兩個都是 number 型別。你可以使用 , 或者 ; 分開屬性,最後一個屬性的分隔符加不加都行。

每個屬性對應的型別是可選的,如果你不指定,預設使用 any 型別。

可選屬性(Optional Properties)

物件型別可以指定一些甚至所有的屬性為可選的,你只需要在屬性名後新增一個 ?

function printName(obj: { first: string; last?: string }) {
  // ...
}
// Both OK
printName({ first: "Bob" });
printName({ first: "Alice", last: "Alisson" });

在 JavaScript 中,如果你獲取一個不存在的屬性,你會得到一個 undefined 而不是一個執行時錯誤。因此,當你獲取一個可選屬性時,你需要在使用它前,先檢查一下是否是 undefined

function printName(obj: { first: string; last?: string }) {
  // Error - might crash if 'obj.last' wasn't provided!
  console.log(obj.last.toUpperCase());
  // Object is possibly 'undefined'.
  if (obj.last !== undefined) {
    // OK
    console.log(obj.last.toUpperCase());
  }
 
  // A safe alternative using modern JavaScript syntax:
  console.log(obj.last?.toUpperCase());
}

聯合型別(Union Types)

TypeScript 型別系統允許你使用一系列的操作符,基於已經存在的型別構建新的型別。現在我們知道如何編寫一些基礎的型別了,是時候把它們組合在一起了。

定義一個聯合型別(Defining a Union Type)

第一種組合型別的方式是使用聯合型別,一個聯合型別是由兩個或者更多型別組成的型別,表示值可能是這些型別中的任意一個。這其中每個型別都是聯合型別的成員(members)

讓我們寫一個函式,用來處理字串或者數字:

function printId(id: number | string) {
  console.log("Your ID is: " + id);
}
// OK
printId(101);
// OK
printId("202");
// Error
printId({ myID: 22342 });
// Argument of type '{ myID: number; }' is not assignable to parameter of type 'string | number'.
// Type '{ myID: number; }' is not assignable to type 'number'.

使用聯合型別(Working with Union Types)

提供一個符合聯合型別的值很容易,你只需要提供符合任意一個聯合成員型別的值即可。那麼在你有了一個聯合型別的值後,你該怎樣使用它呢?

TypeScript 會要求你做的事情,必須對每個聯合的成員都是有效的。舉個例子,如果你有一個聯合型別 string | number , 你不能使用只存在 string 上的方法:

function printId(id: number | string) {
  console.log(id.toUpperCase());
    // Property 'toUpperCase' does not exist on type 'string | number'.
    // Property 'toUpperCase' does not exist on type 'number'.
}

解決方案是用程式碼收窄聯合型別,就像你在 JavaScript 沒有型別註解那樣使用。當 TypeScript 可以根據程式碼的結構推斷出一個更加具體的型別時,型別收窄就會出現。

舉個例子,TypeScript 知道,對一個 string 型別的值使用 typeof 會返回字串 "string"

function printId(id: number | string) {
  if (typeof id === "string") {
    // In this branch, id is of type 'string'
    console.log(id.toUpperCase());
  } else {
    // Here, id is of type 'number'
    console.log(id);
  }
}

再舉一個例子,使用函式,比如 Array.isArray:

function welcomePeople(x: string[] | string) {
  if (Array.isArray(x)) {
    // Here: 'x' is 'string[]'
    console.log("Hello, " + x.join(" and "));
  } else {
    // Here: 'x' is 'string'
    console.log("Welcome lone traveler " + x);
  }
}

注意在 else分支,我們並不需要做任何特殊的事情,如果 x 不是 string[],那麼它一定是 string .

有時候,如果聯合型別裡的每個成員都有一個屬性,舉個例子,數字和字串都有 slice 方法,你就可以直接使用這個屬性,而不用做型別收窄:

// Return type is inferred as number[] | string
function getFirstThree(x: number[] | string) {
  return x.slice(0, 3);
}
你可能很奇怪,為什麼聯合型別只能使用這些型別屬性的交集,讓我們舉個例子,現在有兩個房間,一個房間都是身高八尺戴帽子的人,另外一個房間則是會講西班牙語戴帽子的人,合併這兩個房間後,我們唯一知道的事情是:每一個人都戴著帽子。

TypeScript 系列

  1. TypeScript 之 基礎入門
  2. TypeScript 之 型別收窄
  3. TypeScript 之 函式
  4. TypeScript 之 物件型別
  5. TypeScript 之 泛型
  6. TypeScript 之 Keyof 操作符
  7. TypeScript 之 Typeof 操作符
  8. TypeScript 之 索引訪問型別
  9. TypeScript 之 條件型別

微信:「mqyqingfeng」,加我進冴羽唯一的讀者群。

如果有錯誤或者不嚴謹的地方,請務必給予指正,十分感謝。如果喜歡或者有所啟發,歡迎 star,對作者也是一種鼓勵。

相關文章