
閱讀本篇文章前,仔細想想看
是否會使用 Webpack 建立 TypeScript 專案的環境呢?
貼心小提示
想要本範例的程式碼 —— 可以參見 Maxwell-Alexius/Iron-Man-Competition 這個 Repo 喔!
直接進入正文開始!
在進入 LeafletJS 的使用之前,上一篇已經將簡單的地圖給實踐出來了,筆者快速把上一篇的主程式碼帶過。

其中,筆者已經將 mapConfig 的內容與型別註記整理到 map.config.ts 這個檔案,這樣子主程式不會塞太多型別註記的東西外,如果之後要做調整,就可以直接更改 map.config.ts 裡面的值。

快速看過後我們趕快進到下一關!
通常遇到要視覺化的應用,一定會經過痛苦的資料處理過程。
題外話,這裡附上筆者資料視覺化的作品集,但對筆者而言還不算完整,因為筆者個人目標是把所有的 Chart 都畫出來 XD,但也要看筆者個人時間上的安排。(糟糕偏題了)
今天的目標就是要把台北市的 UBike 資料簡化到我們需要的格式 —— 當然,原本的資料名稱實在是很難看!(如圖一)

圖一:台北市政府的開放資料,對於 UBike 的自行車即時資料的描述內容
光是看欄位名稱:sbi 代表目前的自行車數量、sarea 為行政區域等等,實在是太抽象。今天筆者來 Run 一次自己認為如何用 TypeScript 進行資料處理。
當然,第一步驟就是確保我們能夠接收到資料,以下筆者新增 fetchData.ts 在 /src 資料夾內,裡面內容為:

首先 URL 自然而然是台北市開放 UBike 資料的接口連結,另外 fetchUBikeData 的函式參數,筆者建議不要寫死,因為有可能請求的來源不同。
以下是在 index.ts 裡面引入該函式後,並且進行測試時,檢視資料的狀況。

敏銳的讀者如果讀過編譯器設定篇章 —— 由於我們使用 ES6 Promise 這個物件 —— 此為功能層面(Utility Aspect)而非語法層面的東西,我們必須要在 tsconfig.json 裡的 "lib" 選項改成:
{
  "compilerOptions": {
    /* 略... */
    "lib": ["dom", "es2015"]
    /* 略... */
  }
}
方才可使用 Promise 這些 ES6 以後的功能。
貼心小提示
Promise物件的詳細推論機制,由於牽扯到 Generics 泛用型別機制,因此會在第四篇章講到喔!
儲存之後打開瀏覽器可以發現我們成功地將資料接出來了。(如圖二)

圖二:至少我們先確定資料可以出來了
通常筆者習慣遇到這種亂糟糟的資料,一定會把來源資料的格式,以文件的方式寫出來(省去日後還要上網查詢的時間)。
所以筆者在 fetchData.ts 裡,按照台北市的 UBike 即時資料格式 —— 運用型別化名(Type Alias)寫下來,將它化名成 SourceUBikeInfo。

讀者可能認為以上的格式有寫沒寫都沒差,因為輸出的值全部都是 string 型別 —— 但是筆者建議儘量敘述源頭資料的狀況 —— 筆者這裡告訴 TypeScript 在資料還未處理前,全部的資料都是 string 型別,圖二的開發者工具中的 Console 裡面的內容確實全部都是對應到 string 型別。
第二個步驟就是定義我們的 App 想要轉換成的資料格式,筆者於是再次宣告另一種理想的格式型別化名 UBikeInfo 如下:

第二種 UBikeInfo 是我們希望輸出的型別:
availableBikes 必須從 SourceUBikeInfo 裡的 sbi 欄位取用,並且將 string 轉成 number
totalBikes 必須從 SourceUBikeInfo 裡的 tot 欄位取用,並且將 string 轉成 number
latLng 使用 leaflet 內建的 LatLngExpression 元組型別,必須從 SourceUBikeInfo 的 lat、lng 兩個值 —— 除了轉換成 number 型別外,還要組織成 [number, number] 格式regionName 則是對應 SourceUBikeInfo 裡的 sarea 欄位,不需進行型別轉換stopName 則是對應 SourceUBikeInfo 裡的 sna 欄位,也不需進行型別轉換於是筆者修改 fetchUBikeData 這個函式的內容:

看起來很複雜,筆者分兩段講。
首先,第一個部分:由於本來台北市 UBike 資料長得很奇怪,是 { retVal: [...], retCode: number } 格式,而我們只需要 retVal 這個屬性。
此外,retVal 裡面的值是這種格式:
{
  0001: SourceUBikeInfo,
  0002: SourceUBikeInfo,
  /* 以下略... */
}
筆者希望轉換成 SourceUBikeInfo[] 這種陣列,於是需要使用 Object.keys 結合 map 方法產生成 SourceUBikeInfo[] 的陣列形式。

另外,由以上的程式碼,筆者刻意註記為 retVal[key] as SourceUBikeInfo,理由是 —— 還記得之前筆者強調過:出現 any 型別的情形,其中就以 I/O 相關的機制常見,而 TypeScript 哪會知道你 fetch 到的外部資料格式為何,它當然會自動推論為 any 型別。
因此這並不是在 TypeScript 專案裡樂見的行為。
我們必須在資料轉換的過程中,開始進行型別的積極註記動作喔!
第二步驟:開始進行 SourceUBikeInfo 到 UBikeInfo 的資料格式轉換。

讀者可能覺得這段程式碼好亂,但是這是資料格式轉換必經的過程 —— 不過這裡可以藉由型別的積極註記為 UBikeInfo 的話,不管過程再亂,我們只需要關注資料被轉換的結果有沒有被 TypeScript 監測。
另外,多虧函式的推論機制,我們的 fetchUBikeData 函式的輸出推論結果自動被變成 Promise<UBikeInfo[]>。(如圖三)

圖三:推論為 Promise<UBikeInfo[]>
有些看到這裡的讀者冒出問號:“恩恩恩!?什麼是 Promise<UBikeInfo[]>?難道不是 UBikeInfo[] 而已嗎?”
事實上這是所謂的泛用型別(Generic Type)—— 這會在第四篇章《通用武裝》被筆者討論到。
筆者這邊不負責任先提示:Promise<UBikeInfo[]> 代表的意思是,只要使用 fetchUBikeData 輸出的 Promise 物件,並且使用 then 方法:
fetchUBikeData()
  .then((A) => ...)
參數 A 之型別會被自動推論為 UBikeInfo[]。
貼心小提示
這裡讀者若不知道 ES6 Promise 物件可以暫時看一下社群上的資源。
由於本篇討論的是應用,而非 Promise 物件的主題,所以並不會在本篇介紹過多跟 Promise 甚至是泛用型別相關的東西。
然而,Promise 物件將會在第四篇章《通用武裝》篇章介紹,目的是要讓讀者知道 Generic Types 的威力以及常見性 —— 儘管泛用型別是進階主題,但是這個東西會比讀者想像中還要常遇到。
所以我們可以確定,資料轉換過後的型別註記的部分也在 fetchData.ts 全部處理完畢。
因此在 index.ts 裡:

你可以發現我們的 data 參數被推論結果為 UBikeInfo[],如圖四。

圖四:data 自動被推論為 UBikeInfo[]
這樣的好處就是,如果我們隨便從該陣列資料拉出一個值,VSCode 就會自動幫我們偵測可以使用的屬性呢!(如圖五)

圖五:VSCode 自動幫我們判斷我們可以使用的屬性
除了利用型別系統的註記機制外,然後在主程式能夠不靠註記而只靠型別推論就得知可以使用的功能 —— 這就是筆者想要從 TypeScript 開發過程中想要達到的效率。
打開瀏覽器,我們的資料已經被轉換成好讀的格式。(圖六)

圖六:格式變得美美的~
另外,筆者認為我們還需要把所有台北市的行政區資料提供出來 —— 就稱之為 districtData.ts,並且將其放置在 /src 這個檔案資料夾位置。以下是 ./src/districtData.ts 的內容:

第一個 districts 很簡單,就是一連串的所有行政區。
第二個就比較麻煩一些,但也不會麻煩到哪裡去。districtLatLngMap 使用的是 ES6 Map 這個資料結構 —— 筆者依然在這裡使用類似 ES6 Promise 的型別格式,也是使用泛用型別。
但 Map 可以接受的泛用型別有兩個 —— 第一個代表鍵(key)、第二個則是值(value),它可以這樣被使用:

貼心小提示
同理,ES6 Map 的推論註記行為會在第四篇章討論到!
不過呢,更精確的型別註記應該要先宣告 Districts 這個型別化名為所有行政區的名稱的 union:

貼心小提示
當然,如果覺得這樣很麻煩,讀者也覺得沒有必要將
Districts寫得這麼制式化,可以選擇退化為string型別也 OK。
然後將 districts 跟 districtLatLngMap 改成:

另外,因為我們將台北市行政區所有可能的值設定為 Districts 這個型別,這也代表剛剛的 UBikeInfo 裡的 regionName 可能也必須從 string 改成 Disticts。
可是這樣又要再把 Districts 這個型別額外建立一個檔案,然後再把該化名載入到 fetchData.ts 檔案,不如乾脆我們把所有的型別定義都匯聚在一個宣告檔(Declaration File)好了。
於是筆者在 /src 裡面額外新增 data.d.ts 這個檔案 —— 專門宣告型別的定義。以下是 data.d.ts 的內容:

於是你可以將 Districts、SourceUBikeInfo 與 UBikeInfo 的型別宣告從 fetchData.ts 與 districtData.ts 拔除,並且從 data.d.ts 載入進去。
不過,讀者也可以選擇不要用 data.d.ts 檔案,而是普通的 data.ts 檔案,但改成 export type ... 的方式將型別化名(Type Alias)輸出出去也是可以的!
以下是目前 fetchData.ts 的程式碼大致上的狀況。

以下是目前 districtData.ts 的程式碼大致上的狀況。

這樣子我們就可以把型別化名宣告部分整理到其他的檔案,若讀者想要查詢 UBikeInfo 等等實際上的內容,可以ㄧ樣按照筆者教過的技巧:
UBikeInfo
就會立馬導到你所定義型別化名的地方喔。
最後還是在提醒一下:想要參考完整的程式碼,可以點擊這邊。
今天主要把資料處理的動作都交代清楚了,讀者應該可以很輕易地體會到 TypeScript 型別化名的好處,協助我們確保型別不會弄錯外,我們還可以藉由型別系統寫出 Documentation —— 創造出屬於專案的 Declaration Files 呢!
下一篇我們繼續本案例,將進度推展下去~!