TypeScript入門 - 3

by vivid 17. 十月 2018 05:13

.NET Magazine國際中文電子雜誌
作 者:許薰尹
審 稿:張智凱
文章編號: N181020002
出刊日期: 2018/10/17

在JavaScript中,經常使用鴨子型別(duck typing)來檢查資料值的型別,鴨子型別判斷的依據是物件若含有特定名稱的屬性,它可能就是某種型別。鴨子型別(duck typing)有時也稱做「structural subtyping」。TypeScript介面可以補強鴨子型別,為你的程式碼定義合約(Contract)。

在「TypeScript入門」一文中,已經稍微介紹了TypeScript包含介面(Interface)的用法,而這篇文章中,將要再深入來探討一下TypeScript介面(Interface)進階的語法。

介面(Interface)

TypeScript介面(Interface)可以用於許多用途,介面可以當做抽象型別(abstract type),藉以實作具象類別(concrete classe),此外TypeScript介面也可以用於定義程式的結構。使用「tsc」工具編譯介面程式碼時,並不會產生對應的JavaScript程式碼,介面是使用在程式設計階段,提供工具程式自動完成功能,而在程式編譯階段提供型別檢查。

介面可以用來描述物件具備的屬性,例如以下範例程式碼定義一個「Point」介面,包含兩個「x」、「y」數值型別變數描述座標資料,並將「Point」指定為「func1」函數的輸入參數:

interface Point {
  x: number;
  y: number;
}

let func1 = ( p: Point ) => {
  console.log( p.x, p.y )
}

func1( { x: 10, y: 20 } ) // 10 20


「func1」函式需要傳入一個 「Point」物件當參數,因此最後一行程式碼呼叫「func1」函式時,傳入一個物件,分別指定「x」屬性與「y」屬性的值。上述這段TypeScript程式碼若使用tsc工具程式轉換成JavaScript程式碼,將得到以下結果,介面程式碼並不會產生對應的JavaScript程式碼:

var func1 = function ( p ) {

  console.log( p.x, p.y );

};

func1( { x: 10, y: 20 } ); // 10 20

最後,若在呼叫「func1」函式時,傳入過多或過少引數,或是屬性名稱有誤時,編譯將會失敗,例如以下範例程式碼:

func1( { x: 10 } ) // error,引數過少

func1( { x: 10, y: 20 , z:30 } ) //error,引數過多

func1( { xx: 10, y: 20 } ) /// error,屬性名稱有誤

func1( { x: 10, yy: 20 } ) // error,屬性名稱有誤

行內標記(inline annotation)

對於較簡單的程式碼,可以使用行內標記(inline annotation)語法,例如以下範例程式碼,「func1」函式需要兩個數值參數「x」與「y」,只要在參數名稱「p」後方標註其為物件型別,包含「x」與「y」兩個數值型別屬性即可:

let func1 = ( p: { x: number, y: number } ) => {

  console.log( p.x, p.y )

}

func1( { x: 10, y: 20 } ) // 10 20

行內標記有一個問題,當你叫用到「func1」,但傳入過多的參數時,並不會發生錯誤。編譯器只會檢查「func1」是否含有必要的「x」、「y」參數,參考以下範例程式碼:

let func1 = ( p: { x: number, y: number } ) => {

    console.log( p.x, p.y )

}

let p1 = { x: 10, y: 20, z: 30 };

func1( p1 ) // 10 20

若要避免這個問題,建議使用「interface」明確定義。此外,若是有許多程式碼都會重複使用到兩個「x」與「y」參數,那麼也建議使用「interface」明確定義。

 

選擇性屬性(Optional Properties)

若介面中物件不是所有屬性都是必要的,可以將屬性設計成選擇性屬性(Optional Properties),宣告時在介面屬性名稱後方加上一個「?」符號,就表示此為選擇性屬性,參考以下範例程式碼,「Point」介面的「x」、「y」屬性都是選擇性屬性:

interface Point {
  x?: number;
  y?: number;
}

let func1 = ( p: Point ) => {
  if ( p.x ) {
    console.log( ` x = ${p.x} ` )
  }
  if ( p.y ) {
    console.log( ` y = ${p.y} ` )
  }
}

func1( { x: 10 } ) //  x = 10
func1( { y: 10 } ) //  y = 10
func1( { x:10 , y: 20 } ) //  x = 10 , y = 20
func1( { z:10 }) //  error


使用選擇性屬性的好處是,你可以描述物件具備哪些屬性,並防止存取到物件沒有的屬性。例如範例程式碼最後一行存取到「Point」介面沒有定義的「z」屬性,程式將會無法編譯與執行:

 

唯讀屬性(Readonly Properties)

唯讀屬性(Readonly Properties)只有在第一次建立物件時能夠設定它的值,之後便只能夠讀取,不可以進行修改。參考以下範例程式碼,「Point」介面的「x」、「y」屬性都是唯讀屬性,試圖使用「p1.x = 100」修改「x」屬性時就會發生錯誤:

interface Point {
  readonly x: number;
  readonly y?: number;
}

let p1: Point = { x: 10, y: 20 };
console.log( p1.x ); // 10
console.log (p1.y ); // 20

p1.x = 100; //error

 

使用介面定義函式(Funciton)型別

介面除了可以用來描述物件的屬性之外,也可以用於描述Function型別。參考以下範例程式碼,使用「interface」關鍵字定義一個「displayInfo」函式簽名,要求定義「func1」函式時,參數清單中只包含一個數值型別的參數「empId」;以及一個字串型別的參數「empName」,當叫用「func1」函式時,傳入過多引數時,編譯將會失敗,參考以下範例程式碼:

interface displayInfo {
  (empId: number, empName: string)
}

let func1: displayInfo;
func1 = ( empId: number, empName: string ) => {
  console.log( ` empId = ${empId} ,  empName = ${empName} `)
}

func1( 1, "mary" ) // empId = 1 ,  empName = mary
func1( 1, "mary" ,30 ) // error

 

根據介面來定義函式時,參數名稱不必相符,例如以下範例程式碼,「displayInfo」介面定義函式參數名稱為「empId」與「empName」;而定義「func2」函式時,參數名稱則命為「id」與「name」:

interface displayInfo {
  ( empId: number, empName: string )
}

let func2: displayInfo;
func2 = (id: number, name: string) => {
  console.log(` empId = ${id} ,  empName = ${name} `)
}
func2( 1, "mary" ) // empId = 1 ,  empName = mary

 

函式參數是根據定義的順序來進行檢查,名稱不必相符,若參數順序錯誤則無法編譯,例如以下範例程式碼,「displayInfo」介面中函式的第一個參數的型別是「number」型別;而第二個型別是字串型別,因此「func3」與「func4」都無法編譯:

interface displayInfo {
  ( empId: number, empName: string )
}

let func3: displayInfo;
func3 = ( empName: string, empId: number ) => {  // error
  console.log( ` empId = ${empId} ,  empName = ${empName} ` )
}
func3( "mary", 1 ) // error

let func4: displayInfo;
func4 = ( name: string, id: number ) => {    // error
  console.log( ` empId = ${id} ,  empName = ${name} ` )
}
func4( "mary", 1 )  // error

 

若定義函式時未指定參數型別,則TypeScript會根據參數位置自動推論引數的型別,例如以下範例程式碼:

interface displayInfo {
  ( empId: number, empName: string )
}

let func3: displayInfo;
func3 = ( empId, empName ) => {
  console.log(` empId = ${empId} ,  empName = ${empName} `)
}
func3( 1, "mary" ) //  empId = 1 ,  empName = mary
func3( "mary", 1 ) //  error

let func4: displayInfo;
func4 = ( id, name ) => {
  console.log( ` empId = ${id} ,  empName = ${name} ` )
}
func4( 1, "mary" )  //  empId = 1 ,  empName = mary 
func4( "mary", 1 )  //  error

 

索引型別

介面可以用來描述可索引型別(Indexable Type),指明索引以及傳回值的型別。索引的型別目前支援兩種:「string」與「number」。參考以下範例程式碼:

interface NumberArray {
  [index: number]: number;
}

let ar: NumberArray = [100, 200 , 300];
let x: number = ar[0];
let y: number = ar[1];
let z: number = ar[2];

console.log( x ) // 100
console.log( y ) // 200
console.log( z ) // 300

 

「NumberArray」介面描述索引的型別是數值,傳回值的型別也是數值,使用數值的索引ar[0]、ar[1]、ar[2]取回的值型別也是數值,因此可以直接指定給數值型別的變數「x」、「y」、「z」。

索引的型別實際上會自動轉型為字串來處理,參考以下範例程式碼,將得到相同的結果:

interface NumberArray {
  [index: number]: number;
}

let ar: NumberArray = [100, 200 , 300];
let x: number = ar['0'];
let y: number = ar['1'];
let z: number = ar['2'];

console.log( x ) // 100
console.log( y ) // 200
console.log( z ) // 300

若搭配「readonly」關鍵字來使用,可以限定索引只能用於讀取,不可用於修改值,參考以下範例程式碼:

interface NumberArray {
  readonly [index: number]: number;
}

let ar: NumberArray = [100, 200, 300];
ar[0] = 500; // error

Tags:

.NET Magazine國際中文電子雜誌 | TypeScript | 許薰尹Vivid Hsu

新增評論




  Country flag
biuquote
  • 評論
  • 線上預覽
Loading






NET Magazine國際中文電子雜誌

NET Magazine國際中文電子版雜誌,由恆逸資訊創立於2000,自發刊日起迄今已發行超過500篇.NET相關技術文章,擁有超過40000名註冊讀者群。NET Magazine國際中文電子版雜誌希望藉於電子雜誌與NET Developer達到共同學習與技術新知分享,歡迎每一位對.NET 技術有興趣的朋友們多多支持本雜誌,讓作者群們可以有持續性的動力繼續爬文。<請加入免費訂閱>

月分類Month List