TypeScript 簡明教程:接口、函數與類

本文爲系列文章《TypeScript 簡明教程》中的一篇。git

接口

TypeScript 中,咱們使用接口來描述對象或類的具體結構。接口的概念在 TypeScript 中是相當重要的。它就像是你與程序簽定的一個契約,定義一個接口就意味着你答應程序:將來的某個值(或者類)必定會符合契約中所規定的模樣,若是不符合,TS 就會直接在編譯時報錯es6

感興趣的同窗能夠了解一下 鴨子類型github

舉個例子:編程

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 代碼中。ui

可選屬性

對於某個可能存在的屬性,咱們能夠在該屬性後加上 ?標記 表示這個屬性是可選的。this

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 包括 stringnumber 類型。

只讀屬性

接口中,咱們可使用 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 中可使用三種修飾符:publicprivateprotected

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 會報錯,可是它並不能阻止你訪問這些屬性或方法。

目前有一個提案,建議在語言層面使用 # 前綴標記某個屬性或方法爲私有的,感興趣的能夠看 這裏

抽象類

抽象類是某個類具體實現的抽象表述,做爲其餘類的基類使用。

它具備兩個特色:

  1. 不能被實例化
  2. 其抽象方法必須被子類實現

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
    }
}
複製代碼

接口與抽象類的區別

  1. 類能夠實現(implement)多個接口,但只能擴展(extends)自一個抽象類。
  2. 抽象類中能夠包含具體實現,接口不能。
  3. 抽象類在運行時是可見的,能夠經過 instanceof 判斷。接口則只在編譯時起做用。
  4. 接口只能描述類的公共(public)部分,不會檢查私有成員,而抽象類沒有這樣的限制。

小結

本篇主要介紹了 TypeScript 中的幾個重要概念:接口、函數和類,知道了如何用接口去描述對象的結構,如何去描述函數的類型以及 TypeScript 中類的用法。

相關文章
相關標籤/搜索