- Published on
TypeScript (一):基礎
TypeScript
是 JavaScript
的超集:它是 JavaScript
的語法擴充。原則上,所有 JavaScript
語法都能在 TypeScript
中沿用,只是更加嚴格。
在 TypeScript
中,「型別(type)」被加入程式碼空間中。所謂型別,是對對象的結構的標注與描述。一個具有該型別的對象,擁有該型別所擁有的所有屬性與方法。什麼意思呢?
譬如,一個作為 string
(字串)型別的對象,它應當擁有所有 string
的方法和屬性:
let str: string = 'Hello, TypeScript!'
// str 的型別被標注為 string。
console.log(str.length) // 19
console.log(str.toUpperCase()) // 'HELLO, TYPESCRIPT!'
console.log(str.toFixed(2))
// 不允許,str 沒有 toFixed 方法。
這裡有個重要的觀念:對於 TypeScript
來說,型別的意義,在於這個對象應該擁有哪些屬性和方法,而不在於它叫什麼名字。我們可以將屬性和方法統稱為「結構」,因此這樣的型別又叫「結構性型別」(或有人會叫它「鴨子型別」)。
如果你想寫 TypeScript
,你需要調整 JavaScript
專案的語言配置,這有點麻煩。但你可以在 TypeScript Playground 上進行實驗。
原始型別
有些型別是原始的,包括(下述並非全部,但是最常用的):
// 注意它們都是使用小寫字母。
let str: string = 'Hello, TypeScript!'
let num: number = 123
let float: number = 3.14
let isTrue: boolean = true
當變數宣告時,若使用 let
來進行原始型別的變數宣告,TypeScript
會自動推斷出型別:
let str = 'Hello, TypeScript!'
// str 的型別是 string
let num = 123
// num 的型別是 number
let float = 3.14
// float 的型別是 number
let isTrue = true
// isTrue 的型別是 boolean
但如果使用 const
來進行原始型別的變數宣告,它會將它視作常數。在這種時候,TypeScript
會將其推斷為字面量型別(literal types):
const str = 'Hello, TypeScript!'
// str 的型別是 "Hello, TypeScript!"
const num = 123
// num 的型別是 123
const float = 3.14
// float 的型別是 number
const isTrue = true
// isTrue 的型別是 boolean
要注意的是,在 TypeScript
中,當變數的型別被確定,它便會限制變數的再賦值。這是一個很好的特性,就算你不寫 TypeScript
,或許也該自主遵守這個規約:
let str = 'Hello, TypeScript!'
str = 'Goodbye, TypeScript!'
// 這是可以的,你可以將字串指派為另一個字串。
str = 3
// 這是不行的,你會看到這樣的錯誤,即便這在 JavaScript 中是合法的:
// TypeError: str is not assignable to type 'string'
有一個邏輯上想當然、但重要的特性,那就是字面量型別,即便不使用 const
宣告,TypeScript
也可能阻止對變量的再賦值:
let str: `Hello, TypeScript!` = 'Hello, TypeScript!'
// 使用字面量型別宣告 str。
str = 'Goodbye, TypeScript!'
// 這是不允許的,你會看到錯誤:
// Type '"Goodbye, TypeScript!"' is not assignable to type '"Hello, TypeScript!"'.
never
型別
二元運算子與 如果我們真的希望將字串指派為數字,我們可以使用型別的二元運算子。在下面的例子裡面,我們將 strOrNum
的型別宣告為「字串或數字」(這個運算稱作「聯集運算」):
let strOrNum: string | number = 'Hello, TypeScript!'
strOrNum = 123
// 這是可以的。
另一個二元運算子是「&
」(稱作「交集運算」)。雖然事實上,這兩個型別是不可能共存的,一個變數不可能既是字串,又是數字:
let strAndNum: string & number
這時候如何理解 strAndNum
的型別呢?TypeScript
表示「Type 'string & number' is never
」,意思是,這會是一個 never
的型別。
沒有任何對象能滿足 never
型別。因此,若有一個事物的型別是 never
,代表該對象理論上不該存在。
type
與 interface
二元運算子帶來了無數的變化。如果在我們的程式中,多次需要使用某個運算出來的型別,這會是一個麻煩的事情,譬如說,我們希望定義一個稱之為 state
的變數,用來記錄程式當前的狀態:
let state: 'idle' | 'loading' | 'loaded' | 'error' = 'idle'
如果我們有兩個同樣的型別,一個是 state1
,另一個是 state2
,我們想要這樣寫,但看起來有些累贅:
let state1: 'idle' | 'loading' | 'loaded' | 'error' = 'idle'
let state2: 'idle' | 'loading' | 'loaded' | 'error' = 'idle'
TypeScript
允許我們宣告型別作為獨立的「變數」,這樣寫看起來好多了:
type State = 'idle' | 'loading' | 'loaded' | 'error'
let state1: State = 'idle'
let state2: State = 'idle'
type
用來宣告型別,宣告出來的 State
並非是真正的變數。因此你無法在程式碼中真正使用它。
在心智模式上,你可以把型別當成是獨立於程式的空間,只能用型別的方式表達。
另一種宣告型別的方式是使用 interface
,但和 type
略有不同,它只能用來宣告 JS
物件(object)的「形狀」:
interface State {
type: 'idle' | 'loading' | 'loaded' | 'error'
}
// 在一些比較舊的書或是文件裡面,會建議你以 IState 來命名 interface,但這個建議如今已經過時了。
const state: State = {
type: 'idle',
}
事實上,你也能用 type
宣告物件的形狀,這基本上和 interface
是一樣的:
type State = {
type: 'idle' | 'loading' | 'loaded' | 'error'
}
const state: State = {
type: 'idle',
}
這看起來,type
和 interface
沒什麼差別,但 type
的功能更多,因為無法用 interface
來宣告原始型別或是原始的字面量型別。這樣我們為何還要使用 interface
?
interface
的優勢在於幾個特性:
(1) 首先,它的性能要比 type
好一些,一般來說,建議可以的話先考慮使用 interface
,只有在必要的時候才使用 type
。
(2) 此外,它能夠使用 extends
來定義擴展宣告(但它不能使用二元運算子來定義宣告):
interface State {
type: 'idle' | 'loading' | 'loaded' | 'error'
}
interface AppState extends State {
count: number
}
const state: AppState = {
type: 'idle',
count: 0,
}
如果使用 type
,可以改用下面這段等價的語法(&
表示將兩個物件的屬性和方法加在一起):
type State = {
type: 'idle' | 'loading' | 'loaded' | 'error'
}
type AppState = State & {
count: number
}
const state: AppState = {
type: 'idle',
count: 0,
}
(3) 最後,interface
允許宣告合併(declaration merging):
interface State {
type: 'idle' | 'loading' | 'loaded' | 'error'
}
interface State {
count: number
}
const state: State = {
type: 'idle',
count: 0,
}
宣告合併這個特性是 type
沒有的,而這個特性廣泛用於第三方模組或是全局型別的擴展上。在這裡可以先記住這個特性,你需要在實作中去看到這特性的好處。
陣列
如今我們討論了原始型別以及物件的型別和介面,現在我們來看看陣列。
陣列在 TypeScript
中有兩種,即便它們在 JavaScript
中毫無區別。一種是多元組(tuple),另一種是真正的陣列(array),差別在於,多元組的長度是固定的,陣列的長度是不定的:
const tuple: [string, string] = ['Hello', ' TypeScript']
const wrongTuple: [string, string] = ['Hello', ' TypeScript', ' World']
// 這會出現錯誤,因為宣告的多元組長度是 2。
const array: string[] = ['Hello', ' TypeScript']
tuple
會被 TypeScript
認為是一個長度是 2 的多元組,而 array
則是一個字串陣列。但事實上,TypeScript
並不會頻繁地對陣列進行長度的檢查:
const array: [string, string] = ['Hello', ' TypeScript']
array.push('World')
// TypeScript 允許這個操作,但建議不要進行這樣的操作,這會讓靜態型別檢查失效。
const logArray = (arr: [string, string]): void => {
console.log(arr.join(' '))
}
// 這是函數的型別宣告,代表它收單一的二元多元組作為參數,並且沒有返回值。
logArray(array)
array 雖然長度變成了 3,但 TypeScript
仍然認為它是一個長度是 2 的多元組。
我們可以把 string[]
更改為其他的型別,來表示更多的陣列型別:
const stringArray: string[] = ['Hello', ' TypeScript', 'World']
stringArray.push(1)
// 這會出現錯誤,字串陣列不允許加入數字。
const numberArray: number[] = [1, 2, 3, 4, 5, 6]
const numberMatrix: number[][] = [
[1, 2, 3],
[4, 5, 6],
[7, 8, 9],
]
const stringOrNumberArray: (string | number)[] = ['Hello', ' TypeScript', 1, 2, 3]
type State = 'idle' | 'loading' | 'loaded' | 'error'
const stateArray: State[] = ['idle', 'idle', 'loading', 'loaded']