閱讀本篇文章前,仔細想想看
是否會使用 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 呢!
下一篇我們繼續本案例,將進度推展下去~!