iT邦幫忙

第 11 屆 iThome 鐵人賽

DAY 4
5
Modern Web

讓 TypeScript 成為你全端開發的 ACE!系列 第 4

Day 04. 前線維護・函式型別 X 積極註記 - Function Types

  • 分享至 

  • twitterImage
  •  

https://ithelp.ithome.com.tw/upload/images/20190914/20120614PW9YYyhNhe.png

閱讀本篇文章前,仔細想想看

  1. 大概可以解釋普通 JS 物件(也就是 JSON 格式,或筆者所謂的狹義物件)在 TypeScript 裡的推論機制。
  2. 知道筆者表達的廣義跟狹義物件的差別在哪裡嗎~?(儘管這不是寫程式圈子裡的正統詞彙,但卻是筆者用來好說明本系列文章而衍生出來的詞)
  3. 為何我們不常直接對變數做 object 型別的註記?

如果還沒理解完畢的話,可以先翻看前一篇文章喔!

[2019.09.15 新增] tsconfig.json 設定

這裡筆者必須緊急說明:若讀者試著筆者舉的程式碼範例的話,請讀者記得將裡面的 strictNullCheck 選項改成 true,這一點忘記在文章系列的一開頭提醒讀者,實在是很抱歉!

/* tsconfig.json */
{
  "compilerOptions": {
    /*  ...  */
    "strictNullChecks": true,
    /* ... */
  }
}

因此請讀者注意,目前學習的 TypeScript 型別系統版本多了一個 strictNullCheck 的編譯器屬性設定!至於為何會造成如此狀況,那是因為筆者在專案上習慣將某些 TypeScript 編譯器設定啟動!至於 strictNullCheck 到底為何,將會在型別系統講述告一段落後,開始講述 TypeScript 的編譯器設定檔喔!

[2019.09.18 新增] 程式碼範例

如果想要看到本系列文裡面舉的程式碼範例可以參考 Maxwell-Alexius/Iron-Man-Competition 這個 GitHub Repo 喔~寫作過程當中會不斷更新的!

在進入正文之前,我們再把前一篇得知的重要定律給重新看過一次:

前一篇推演出的結論:廣義物件完整性定律

廣義物件在被 TypeScript 推論的狀態下,屬性不能被任意新增或更改成其他型別。能夠做的事情只有:

  1. 全面覆寫,廣義物件的屬性對照型別格式也要完全對位
  2. 更改廣義物件本身就擁有屬性對應的值,其中:要帶入的值的型態必須對應到該屬性的型態

我們稱這樣的行為為「保持廣義物件的完整性」。

稍微有印象後,那麼我們就 ...

正文開始!

物件型別之函式型別解析 Object Types

環境沿用

一樣沿用前一天的環境,因此如果有任何問題可以參考前一篇

函式型別 Function Types

今天可以講得稍微輕鬆一些了,來看看函式型別的推論與註記吧!先從最基本的開始。(結果如圖一)

// index.ts
let aSimpleFunction = function() { console.log('Hi!'); };

https://ithelp.ithome.com.tw/upload/images/20190912/20120614Dfubj7dMU4.png
圖一:函式物件的推論結果

嗯~我想大部分的讀者應該對這種格式並不陌生:() => void,這個函式格式 —— 它的輸入端是空的,輸出端是 void 代表的也是空值,或者代表不回傳的狀態

再來一個簡單範例,比如:

const addition = function (num1, num2) {
  return num1 + num2;
};

結果呢,我們被 TS 警告了(圖二為顯示錯誤的點,圖三為錯誤訊息),這邊的點就很重要了!(哪裡的點?請不要亂想!)

https://ithelp.ithome.com.tw/upload/images/20190912/20120614fAqotomVUN.png
圖二:TS 明顯對於宣告過後的函數參數(Parameters)很有意見

https://ithelp.ithome.com.tw/upload/images/20190912/20120614PHituqdNEK.png
圖三:原來,TS 還是會介意參數,只是我們看到神秘的錯誤訊息(其實說神秘理解過後一點也不神秘)

Implicit any

TypeScript 對於函式參數(Parameters)的推論結果是 —— any

不過讀者可能想說,如果是 any 的話,TypeScript 不是不管的嗎?

這裡我們就要反推一下,如果 TypeScript 完全對函式參數不予理會的話,首先第一個想得到的問題是:

它到底要如何推論出函式輸出的型別呢?通常是先知道輸入的型別是什麼,才能間接推論輸出的型別吧

第二個問題 —— 典型的蛋生雞、雞生蛋的概念 —— 站在 TypeScript 的立場想想看:

如果你是 TS,你真的能夠推論函式應該要放入的型別是什麼嗎?

那麼假設今天開發者想要讓 addition 函式不是用在數字上,而是字串上的連接。但光是看到:

const addition = function (param1, param2) {
  return param1 + param2;
};

TS 根本無從推論說你到底是要讓函式的輸入作為什麼型別,因此得到最後的結論 —— 把參數一率推論為 any,不過 TypeScript 照樣會提醒:“你是不是忘記對參數註記型別?如果是這樣我只能把這個函式的參數當成 any 囉?”

如果 TS 真的把函式的參數當成 any,那讀者可真的就得小心了。

在這裡我們強制將參數註記為 any,以下的程式碼連 TypeScript 也不會想鳥你的。(結果如圖四)

https://ithelp.ithome.com.tw/upload/images/20190914/20120614ezbizESELL.png

https://ithelp.ithome.com.tw/upload/images/20190912/20120614BFSZg4kkCY.png
圖四:筆者明知故犯的錯誤,TypeScript 也沒有發出任何警訊,但這是錯誤的行為

因此筆者很感謝 TypeScript 留意的這一類潛在 Bug 出現的狀況:在 TypeScript 的世界裡,我們稱這個現象為 Implicit any (隱性的 any 推論,中文真的很難翻,我們之後還是用 Implicit Any 來代表我們遇到的這種問題吧!)

我們重新描述一下這個問題以及對 Implicit Any 下一個定義:

重點 1. Implicit Any

大部分的情況下,只要定義任何函式,TypeScript 通常會無條件推論函式內的參數(Parameters)為 any 型別,這種現象我們稱之為 Implicit Any。

讀者試試看

請讀者試著回想一下,過往有沒有在使用 JavaScript 開發的過程,犯過類似的錯誤?

這裡舉筆者遇過的例子,我們通常會把 addition 這個函式當成是對數字做相加。但筆者踩過的雷是:有時候數字的來源 —— 比如呼叫後端 Server 出來的 JSON Response,明明內容是數字,但解析出來的結果是字串類型(例如:字串的 "42" 而非數字的 42)。

在這種情況下筆者沒注意到,結果要除錯就真的麻煩了,數字的字串加數字的字串還是等於長得很像數字的字串,但是是那種沒數字計算意義上的行為。

let number1 = addition('3', '4');
// => '34'

let number2 = addition(3, 4);
// => 7

哇,這在除錯上更為難人,要是 TypeScript 能夠提前警告筆者定義的 addition 函式裡的參數只能接收數字型態的值,根本就不用再走這除 Bug 冤枉路!

TypeScript 在大部分的狀況下會搞不清楚函式裡參數的型別是什麼,因此會對我們發出 Implicit any 的警訊。不過筆者還是強調一下這幾個字:

“在大部分的情況下”

那是什麼情況下,TS 認得函式裡參數的型別呢?這裡筆者就先埋這個梗(很快就會在後續的篇章有答案!所以我們要看下去~),讓讀者想想看,站在 TS 的角度下,你會在什麼情況下可以直接篤定地推論出某函式的參數它的型別呢

可能有些讀者會覺得麻煩:“我今天是來看教學文,我不想傷我的腦”。其實筆者也可以選擇直接講出答案,但是這樣就缺少了自由思考的空間。如果讀者大概想過,再回來讀文章,相信會把印象烙得更深刻喔!

回顧一下(筆者的廢話可以跳過)

講到這裡,筆者光是把型別推論(Type Inference)從原始型別、狹義物件到現在的函式物件分析過後,是不是覺得比想像中複雜呢?

筆者事實上也這麼覺得,因此花了很多時間為了一個 Inference Behaviour in TypeScript 整理到寫文章中途也有點亂掉的狀態。

但是基礎不打好的話,就不能篤定說:“我們是真正理解 TypeScript 型別系統的人”。看來要成為型別系統達人的話,不是隨隨便便地跟別人說:“有這些工具可以用”,結果推論機制的細節以及到底要在哪裡註記也講不出來。

這樣實在是不行,這對筆者來說實在是太膚淺(相信對讀者來說也很膚淺 XD),既然要學好一門東西還是得把全局掌握著會比較好。

函式的型別推論與註記

從剛剛筆者呈現的案例得知被定義出來的函式到底有多脆弱,很容易就被開發者誤用!

在沒有認清或協議好函式到底要怎麼用的狀況(型別只能接受什麼?怎麼正確使用?Document 有沒有寫好?真的沒有例外嗎?我可以亂填東西嗎?),只會造成更多亂七八糟的補丁狀況,比如:把各種型別狀況用一個個判斷敘述給處理掉,或直觀一點 —— 丟出例外 throw new Error 等等。

函式的參數註記(Parameter Type Annotation)其實很簡單。想要使剛剛的 addition 函數的參數只能接受數字型別的話,這樣做就可以了:

https://ithelp.ithome.com.tw/upload/images/20190914/20120614iIvHgUzMhM.png

神奇的事情在後頭,如果再把剛剛錯誤的範例更正,變成這樣(結果如圖五,錯誤訊息在圖六):

https://ithelp.ithome.com.tw/upload/images/20190914/20120614xBRpD4qlBd.png

https://ithelp.ithome.com.tw/upload/images/20190912/20120614p2iNySVHlQ.png
圖五:正確的函式參數註記方法

https://ithelp.ithome.com.tw/upload/images/20190912/20120614pm6ZquvC42.png
圖六:函式的輸出可以藉由輸入的型別來推論

TypeScript 在我們還沒註記函式的輸出部分(沒錯,也可以註記函式輸出的型別喔!)就已經幫我們把輸出的型別藉由輸入參數的型別推論出來了,因此我們可以很安心地使用這個函式。

在此我們可以下另外一個結論:

重點 2. 函式的推論與註記

分別為輸入參數與輸出部分,大部分情況下,只要我們提供函式參數的註記,輸出就可以間接被 TypeScript 推論出來

那麼哪些是就算我們有了參數註記卻也不能得知輸出型別的狀態呢?

探討這個問題前,我們先來看看輸出部分到底如何註記(Return Type Annotation)。其實還是蠻簡單的:

https://ithelp.ithome.com.tw/upload/images/20190914/20120614gAmD33dK2s.png

好的,那讀者會問:“真的有函式可以設計到回傳的型別是不確定或未知的嗎?”。

有的!有些情況設計出來的函式,其回傳的型別只能是 any

有些人可能就繼續問:“誒!可是你不是說我們必須儘量避免 any 嗎?”

這個就是筆者形容極少數狀況下會使用 any 的其中一種 Case:請看看 JSON.parse 這個函式(確切來說是方法,畢竟方法跟函式還是有差別)到底被 TS 推論出什麼?(圖七)

https://ithelp.ithome.com.tw/upload/images/20190912/201206141cILxwJxHI.png
圖七:哇,好長一串,但重點在於它的輸出型別是 any

仔細看一下這段:

(method) JSON.parse(text: string, reviver?: ((this: any, key: string, value: any) => any) | undefined): any

我們整理一下:

https://ithelp.ithome.com.tw/upload/images/20190914/20120614798XTuY9m8.png

讀者看到有個 reviver? 對應的是另一種看起來是函式型別的東西,而且該型別還加 | 代表 union。這個部分會在後續講到 Optional Properties / Parameters(選用屬性或參數,中文有點難聽,英文比較適合)以及明文型別(Literal Type)會再多多說明喔~

(以下就不稱 JSON.parse 為函式,不然會誤導一些讀者。至於不知道函式跟方法的差別的讀者,請自行上網查詢,因為不在本系列重點)

這裡的重點是:我們看到了這個方法回傳的型別是 any,其實用過 JSON.parse 應該也會覺得合情合理,畢竟 JSON 格式就有無限多種,除了用 object 作為回傳格式外,應該也只能用 any 來表示。

這時候就要跟讀者說,遇到這種要把 any 型別的值帶入某變數 —— 可以加上型別註記來讓 TypeScript 發揮作用(這裡的型別註記也是用明文型別做範例,因此等我們講到明文型別會再補充),你會發現以下所有的狀況 TS 都接受(結果如圖八):

https://ithelp.ithome.com.tw/upload/images/20190914/20120614Qa6vRUOHSO.png

https://ithelp.ithome.com.tw/upload/images/20190912/20120614d7dO0zq0vw.png
圖八:三種告訴 TS 編譯器,我們的變數應該要是什麼型別或長什麼樣子

如果你不這麼做,就會違反在討論原始型別篇章時一開始講的:“儘量不要讓變數被推論為 any 狀態”(如圖九)

https://ithelp.ithome.com.tw/upload/images/20190912/20120614lwppe1IbWs.png
圖九:畢竟你不跟 TS 說,TS 也只能將函數回傳的 any 型別套到變數身上

但如果把型別註記加上去,情況就不同了。(如圖十)

https://ithelp.ithome.com.tw/upload/images/20190912/2012061482rkorXTbI.png
圖十:加上了型別註記,就等同於讓 TypeScript 幫我們關注型別喔,撿回使用 TS 的根本優勢

重點 3. 函式回傳 any 型別

遇到函式是回傳 any 型別的值,我們必須主動對該值作型別註記(Type Annotation),找回開發 TypeScript 的優勢 —— 也就是 TS 提供的型別系統(Type System)

函式型別的覆寫

如果讀者真的很清楚筆者整理的“廣義物件完整性定律”的話 —— 我們可以完全覆寫函式型別,只要格式正確,TS 就很安全地給過!(範例如圖十一的,圖十二和十三分別為兩種案例的錯誤情形)

https://ithelp.ithome.com.tw/upload/images/20190912/20120614w5TWqH25oD.png
圖十一:驗證廣義物件完整性的定律,結論是格式一但錯誤就不能被覆寫

https://ithelp.ithome.com.tw/upload/images/20190912/20120614ZghQyeZnUx.png
圖十二:參數型別錯了,因此被 TS 提醒

https://ithelp.ithome.com.tw/upload/images/20190912/20120614lZI0F9H34x.png
圖十三:就連忘記回傳值也會被提醒喔

另一種情形是 —— 根據廣義物件完整性定律,我們也可以覆寫函數物件的屬性,但是應該沒人想過要這樣做吧...

// 想試試看覆寫掉 Function.prototype.bind 之類的方法嗎?
addition.bind = function ...

筆者在這裡認為,覆寫掉函式的屬性(或方法,畢竟方法也算是一種屬性,只是該屬性對應的值是一個函式)—— 除非讀者有特別需求,否則筆者實在是想不透為何要這麼做,因此這裡就不提供覆寫函式物件屬性的驗證囉。

函式不回傳值的狀態:void

回想剛剛其中的範例,我們注意到:

https://ithelp.ithome.com.tw/upload/images/20190914/201206142sCFbzGo3D.png

儘管有些讀者認為 —— 函式不回傳值就是 void 型別 —— 這點被視為理所當然的,不過筆者還是整理並且強調一下:

重點 4. 函式不回傳值的型態

若定義的函式不回傳值的話,不管有沒有被註記,型別推論結果會被認定為 void

讀者試試看

筆者這邊讓讀者想想看並主動實驗一下,以下的範例哪些會被 TS 認為錯誤?如果 TS 通過的話,那型別推論結果會是什麼?

https://ithelp.ithome.com.tw/upload/images/20190914/20120614Vbnu6H6BQm.png

小結

今天總算把函式物件的型別在 TypeScript 裡面被推論和註記的機制都了解完畢囉~ 但別忘記了,我們還有陣列這東西要搞定,敬請期待下一篇的分析~


上一篇
Day 03. 前線維護・物件型別 X 完整性理論 - Object Types Basics
下一篇
Day 05. 前線維護・陣列型別 X 型別陣列 - Array Types
系列文
讓 TypeScript 成為你全端開發的 ACE!51
圖片
  直播研討會
圖片
{{ item.channelVendor }} {{ item.webinarstarted }} |
{{ formatDate(item.duration) }}
直播中
0
lolis
iT邦新手 5 級 ‧ 2020-05-03 22:57:14

hello,請教下

let strJson = '{"name":"周星馳","age":23}';
// let strJsonObj = <{ name: number; age: string }>JSON.parse(strJson);

那麼就不符合他的輸出型別啦~ts也不會報錯,那怎麼可好?

PeterLiao iT邦新手 4 級 ‧ 2020-06-01 10:48:46 檢舉

parse出來是any所以不會報錯。
這邊是因為示範,所以我們自己預設一筆已知型別的資料。

真實情況則是:
我們接到的資料非常有可能不會符合我們要的型別格式,
通常會再視需求重塑,因此在接到資料時還會再檢驗或轉型處理

以上是我的看法,歡迎交流及指正~

1
良葛格
iT邦新手 2 級 ‧ 2021-12-20 11:38:50

如果讀者真的很清楚筆者整理的“廣義物件完整性定律”的話 —— 我們可以完全覆寫函式型別,只要格式正確,TS 就很安全地給過!

這邊所謂函式的格式,應該是指 function signature 吧!一般翻為函式簽署;另外:

let addition = function(param1: number, param2: number) {
    return param1 + param2;
};

addition = function(param1: number, param2: number) {
    return param2 + param1;
};

並沒有覆寫 addition 的型別,只是重新指定了另一個函式值,addition 的型別依舊是 function(number, number): number

PS. VSCode 會把 addition 的型別顯示為 function(param1: number, param2: number): number,不過那是不對的,參數名稱一般不被視為函式簽署的一部份,事實上 TypeScript 也不這麼認為。

0
WILL.I.AM
iT邦新手 3 級 ‧ 2022-02-06 18:13:45

圖十一裡的行號241行的註解說明有錯誤, 不是參數型別錯誤, 而是回傳值型別錯誤

我要留言

立即登入留言