本文為系列文章《TypeScript 簡明教程》中的一篇。
介面
TypeScript 中,我們使用介面來描述物件或類的具體結構。介面的概念在 TypeScript 中是至關重要的。它就像是你與程式簽訂的一個契約,定義一個介面就意味著你答應程式:未來的某個值(或者類)一定會符合契約中所規定的模樣,如果不符合,TS 就會直接在編譯時報錯。
感興趣的同學可以瞭解一下 鴨子型別。
舉個例子:
interface Phone {
model: string
price: number
}
let newPhone: Phone = {
model: 'iPhone XS',
price: 8599,
}
複製程式碼
上面的例子中,我們定義了一個介面 Phone
,它約定:任何型別為 Phone
的值,有且只能有兩個屬性:string
型別的 model
屬性以及 number
型別的 price
屬性。之後,我們宣告瞭一個變數 newPhone
,它的型別為 Phone
,而且遵照契約,將 model
賦值為字串,price
賦值為數值。
介面一般首字母大寫。在某些程式語言會建議使用
I
作為字首。關於是否要使用I
字首,tslint 有一條 專門的規則,請根據團隊編碼風格自行選擇。
多一些屬性和少一些屬性都是不允許的。
let phoneA: Phone = {
model: 'iPhone XS',
} // Error: Property 'price' is missing in type '{ model: string; }' but required in type 'Phone'
let phoneB: Phone = {
model: 'iPhone XS',
price: 8599,
producer: 'Apple',
} // Error: Property 'producer' doesn't exist on type `Phone`.
複製程式碼
介面作為型別註解,只在編譯時起作用,不會出現在最終輸出的 JS 程式碼中。
可選屬性
對於某個可能存在的屬性,我們可以在該屬性後加上 ?標記
表示這個屬性是可選的。
interface Phone {
model: string
price: number
producer?: string
}
let newPhone: Phone = {
model: 'iPhone XS',
price: 8599, // OK
}
let phoneB: Phone = {
model: 'iPhone XS',
price: 8599,
producer: 'Apple', // OK
}
複製程式碼
任意屬性
某些情況下,我們可能只知道介面中部分屬性及它們的型別。或者,我們希望能夠在初始化後動態新增物件的屬性。這時,我們可以使用下面這種寫法。
interface Phone {
model: string
price: number
producer?: string
[propName: string]: any
}
let phoneB: Phone = {
model: 'iPhone XS',
price: 8599,
}
phoneB.storage = '256GB' // OK
複製程式碼
上面,我們定義任意屬性的簽名為 string
型別,值為 any
型別。注意:任意屬性的值型別必須包含所有已知屬性的值型別。 上述例子中,any
包括 string
和 number
型別。
只讀屬性
介面中,我們可以使用 readonly
標記某個屬性是隻讀的。當我們試圖修改它時,TS 會提示報錯。
interface Phone {
readonly model: string
price: number
}
let phoneA: Phone = {
model: 'iPhone XS',
price: 8599,
}
phoneA.model = 'iPhone Air' // Error: Cannot assign to 'model' because it is a read-only property.
複製程式碼
函式
呼,終於說到函式了。JavaScript 中有兩種定義函式的方法。
// 命名函式
function add(x, y) {
return x + y
}
// 匿名函式
const add = function(x, y) { return x + y }
複製程式碼
對於這兩種方法,新增型別註釋的方式大同小異。
// 命名函式
function add(x: number, y: number): number {
return x + y
}
// 匿名函式
const add = function(x: number, y: number): number {
return x + y
}
複製程式碼
上面我們定義了 add
函式,它接受兩個 number
型別的引數,並規定其返回值為 number
型別。
呼叫函式時,傳入引數的型別和數量必須與定義時保持一致。
add(1, 2) // OK
add('1', 0) // Error
add(1, 2, 3) // Error
複製程式碼
可選引數
使用 ?標記
可以標識某個引數是可選的。可選引數必須放在必要引數後面。
function increment(x: number, step?: number): number {
return x + (step || 1)
}
increment(10) // => 11
複製程式碼
引數預設值
ES6 允許我們為引數新增預設值。作為 JS 的超集,TS 自然也是支援引數預設值的。
function increment(x: number, step: number = 1): number {
return x + step
}
increment(10) // => 11
複製程式碼
因為具有引數預設值的引數必然是可選引數,所以無需再使用 ?
標記該引數時可選的。
這裡,step: number = 1
可以簡寫為 step = 1
,TS 會根據型別推斷自動推斷出 step
應為 number
型別。
與可選引數不同的是,具有預設值的引數不必放在必要引數後面。下面的寫法也是允許的,只是在呼叫時,必須明確地傳入 undefined
來獲取預設值。
function increment(step = 1, x: number): number {
return x + step
}
increment(undefined, 10) // => 11
複製程式碼
剩餘引數
ES6 允許我們使用剩餘引數將一個不定數量的參數列示為一個陣列。TypeScript 中我們可以這樣寫。
function sum(...args: number[]): number {
return args.reduce((prev, cur) => prev + cur)
}
sum(1, 2, 3) // => 6
複製程式碼
注意與 arguments
物件進行 區分。
介面中的方法
對於介面中的方法,我們可以使用如下方式去定義:
interface Animal {
say(text: string): void
}
// 或者
interface Animal {
say: (text: string) => void
}
複製程式碼
這兩種註解方法的效果是一致的。
函式過載
函式過載允許你針對不同的引數進行不同的處理,進而返回不同的資料。
因為 JavaScript 在語言層面並不支援過載,我們必須在函式體內自行判斷引數進行鍼對性處理,從而模擬出函式過載。
function margin(all: number);
function margin(vertical: number, horizontal: number);
function margin(top: number, right: number, bottom: number, left: number);
function margin(a: number, b?: number, c?: number, d?: number) {
if (b === undefined && c === undefined && d === undefined) {
b = c = d = a
} else if (c === undefined && d === undefined) {
c = a
d = b
}
return {
top: a,
right: b,
bottom: c,
left: d,
}
}
console.log(margin(10))
// => { top: 10, right: 10, bottom: 10, left: 10 }
console.log(margin(10, 20))
// => { top: 10, right: 20, bottom: 10, left: 20 }
console.log(margin(10, 20, 20, 20))
// => { top: 10, right: 20, bottom: 20, left: 20 }
console.log(margin(10, 20, 20))
// Error
複製程式碼
上述例子中,前面三個宣告瞭三種函式定義,編譯器會根據這個順序來處理函式呼叫,最後一個為最終的函式實現。需要注意的是,最後的函式實現引數型別必須包含之前所有的引數型別定義。因此,在定義過載的時候,一定要把最精確的定義放在最前面。
類
以前,JavaScript 中並沒有類的概念,我們使用原型來模擬類的繼承,直到 ES6 的出現,引入了 class
關鍵字。如果你對 ES6 的 class
還不是很瞭解,建議閱讀 ECMAScript 6 入門 - Class。
TypeScript 除了實現了所有 ES6 中的類的功能以外,還新增了一些新的用法。
訪問修飾符
TypeScript 中可以使用三種修飾符:public
、private
、protected
。
public
修飾符
表示屬性或方法是公有的,在類內部、子類內部、類的例項中都能被訪問。預設情況下,所有屬性和方法都是 public
的。
class Animal {
public name: string
constructor(name) {
this.name = name
}
}
let cat = new Animal('Tom')
console.log(cat.name); // => Tom
複製程式碼
private
修飾符
表示屬性或方法是私有的,只能在類內部訪問。
class Animal {
private name: string
constructor(name) {
this.name = name
}
greet() {
return `Hello, my name is ${ this.name }.`
}
}
let cat = new Animal('Tom')
console.log(cat.name); // Error: 屬性“name”為私有屬性,只能在類“Animal”中訪問。
console.log(cat.greet()) // => Hello, my name is Tom.
複製程式碼
protected
修飾符
表示屬性或方法是受保護的,與 private
近似,不過被 protected
修飾的屬性或方法也能被其子類訪問。
class Animal {
protected name: string
constructor(name) {
this.name = name
}
}
class Cat extends Animal {
constructor(name) {
super(name)
}
greet() {
return `Hello, I'm ${ this.name } the cat.`
}
}
let cat = new Cat('Tom')
console.log(cat.name); // Error: 屬性“name”受保護,只能在類“Animal”及其子類中訪問。
console.log(cat.greet()) // => Hello, I'm Tom the cat.
複製程式碼
注意,TypeScript 只做編譯時檢查,當你試圖在類外部訪問被 private
或者 protected
修飾的屬性或方法時,TS 會報錯,但是它並不能阻止你訪問這些屬性或方法。
目前有一個提案,建議在語言層面使用
#
字首標記某個屬性或方法為私有的,感興趣的可以看 這裡。
抽象類
抽象類是某個類具體實現的抽象表述,作為其他類的基類使用。
它具有兩個特點:
- 不能被例項化
- 其抽象方法必須被子類實現
TypeScript 中使用 abstract
關鍵字表示抽象類以及其內部的抽象方法。
繼續使用上面的 Animal
類的例子:
abstract class Animal {
public abstract makeSound(): void
public move() {
console.log('Roaming...')
}
}
class Cat extends Animal {
makeSound() {
console.log('Meow~')
}
}
let tom = new Cat()
tom.makeSound() // => 'Meow~'
tom.move() // => 'Roaming...'
複製程式碼
上述例子中,我們建立了一個抽象類 Animal
,它定義了一個抽象方法 makeSound
。然後,我們定義了一個 Cat
類,繼承自 Animal
。因為 Animal
定義了 makeSound
抽象類,所以我們必須在 Cat
類裡面實現它。不然的話,TS 會報錯。
// Error: 非抽象類“Cat”沒有實現繼承自“Animal”類的抽象成員“makeSound”。
class Cat extends Animal {
meow() {
console.log('Meow~')
}
}
複製程式碼
類與介面
類可以實現(implement)介面。通過介面,你可以強制地指明類遵守某個契約。你可以在介面中宣告一個方法,然後要求類去具體實現它。
interface ClockInterface {
currentTime: Date
setTime(d: Date)
}
class Clock implements ClockInterface {
currentTime: Date
setTime(d: Date) {
this.currentTime = d
}
}
複製程式碼
介面與抽象類的區別
- 類可以實現(
implement
)多個介面,但只能擴充套件(extends
)自一個抽象類。 - 抽象類中可以包含具體實現,介面不能。
- 抽象類在執行時是可見的,可以通過
instanceof
判斷。介面則只在編譯時起作用。 - 介面只能描述類的公共(
public
)部分,不會檢查私有成員,而抽象類沒有這樣的限制。
小結
本篇主要介紹了 TypeScript 中的幾個重要概念:介面、函式和類,知道了如何用介面去描述物件的結構,如何去描述函式的型別以及 TypeScript 中類的用法。