TypeScript 的官方文件早已更新,但我能找到的中文文件都還停留在比較老的版本。所以對其中新增以及修訂較多的一些章節進行了翻譯整理。
本篇翻譯整理自 TypeScript Handbook 中 「Everyday Types」 章節。
本文並不嚴格按照原文翻譯,對部分內容也做了解釋補充。
常見型別(Everyday Types)
本章我們會講解 JavaScript 中最常見的一些型別,以及對應的描述方式。注意本章內容並不詳盡,後續的章節會講解更多命名和使用型別的方式。
型別可以出現在很多地方,不僅僅是在型別註解 (type annotations)中。我們不僅要學習型別本身,也要學習在什麼地方使用這些型別產生新的結構。
我們先複習下最基本和常見的型別,這些是構建更復雜型別的基礎。
原始型別: string
,number
和 boolean
(The primitives)
JavaScript 有三個非常常用的原始型別:string
,number
和 boolean
,每一個型別在 TypeScript 中都有對應的型別。他們的名字跟你在 JavaScript 中使用 typeof
操作符得到的結果是一樣的。
string
表示字串,比如 "Hello, world"number
表示數字,比如42
,JavaScript 中沒有int
或者float
,所有的數字,型別都是number
boolean
表示布林值,其實也就兩個值:true
和false
型別名
String
,Number
和Boolean
(首字母大寫)也是合法的,但它們是一些非常少見的特殊內建型別。所以型別總是使用string
,number
或者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)
當你使用 const
、var
或 let
宣告一個變數時,你可以選擇性的新增一個型別註解,顯式指定變數的型別:
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 });
這裡,我們給引數新增了一個型別,該型別有兩個屬性, x
和 y
,兩個都是 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 系列
- TypeScript 之 基礎入門
- TypeScript 之 型別收窄
- TypeScript 之 函式
- TypeScript 之 物件型別
- TypeScript 之 泛型
- TypeScript 之 Keyof 操作符
- TypeScript 之 Typeof 操作符
- TypeScript 之 索引訪問型別
- TypeScript 之 條件型別
微信:「mqyqingfeng」,加我進冴羽唯一的讀者群。
如果有錯誤或者不嚴謹的地方,請務必給予指正,十分感謝。如果喜歡或者有所啟發,歡迎 star,對作者也是一種鼓勵。