閒聊 TypeScript 這個東西與為什麼用

閒聊 TypeScript 這個東西與為什麼用

前言

基本上 TypeScript 已經變成了許多開發者的必備技能之一,但是實際上專案滿多專案都還是處於 JavaScript 的狀態,並猶豫著要不要轉換成 TypeScript,所以這一篇我就來簡單分享自己對於 TypeScript 的看法,以及為什麼要用 TypeScript。

關於 TypeScript

TypeScript 是一個由微軟開發並維護的開源語言…

「恩?所以代表 TypeScript 是一個全新的程式語言嗎?」

不全然,雖然 TypeScript 有許多的特性,以及自己的語法,但是本質上你在開發時,還是要遵循著 JavaScript 的規範,因為 TypeScript 本質上是 JavaScript 的超集合(SuperSet),所以你在開發 TypeScript 時,你可以使用 JavaScript 的所有語法,但是反過來就不行了,因為 JavaScript 並不認識 TypeScript 的語法。

那麼剛剛有提到 TypeScript 它是 JavaScript 的超集合,所以其實本質上還是一個 JavaScript 這件事情,但…什麼是「超集合(SuperSet)」有些人可能不太懂什麼叫做超集合(SuperSet),所以這邊我們先來講一下什麼是超集合。

對於一名普通的前端開發者來講,大多都是使用 JavaScript 來實作開發,然後 JavaScript 又有區分 ES5、ES6、ES7…等等(ECMAScript),對於前端開發者來講,每次我們在使用新語法的時候,都必須要考慮到瀏覽器的相容性,因為不是每一個瀏覽器都支援最新的語法,所以我們必須要使用 Babel 來幫我們轉換成瀏覽器可以支援的語法,所以就會變得非常麻煩(每一代的 ECMAScript 語法都必須要等到瀏覽器支援後才能使用)。

而每一代的 ECMAScript 語法都是一個集合(Set),也就是說每一代的 ECMAScript 語法都包含了上一代的語法,如下圖:

ECMAScript 集合

也就是說 ECMAScript 都是一個集合,你不可能到 ES7 的時候,結果你只能使用 ES7 的語法,而不能使用 ES5 的語法吧?所以每一代的 ECMAScript 都是一個集合,而每一代的 ECMAScript 都包含了上一代的語法。

那麼對於集合有一定概念後,讓我們回來看看人家常常在講的這句話

「TypeScript 是 JavaScript 的超集合(SuperSet)」

什麼是超集合(SuperSet),前面有提到 TypeScript 在撰寫時,還是要遵循著 JavaScript 的規範,所以 TypeScript 本質上是還是一個 JavaScript,那這樣子 TypeScript 為什麼還會被稱之為超集合(SuperSet)呢?剛剛前面就已經有提到過,每一代的 ECMAScript 都是一個集合,而每一代的 ECMAScript 都包含了上一代的語法,所以 TypeScript 一定會包含了 ES5、ES6、ES7…等等的語法,但這時候你應該會這樣想吧?

「如果 TypeScript 只是包含了 ES5、ES6、ES7…等等的語法,那也只是一個集合而已,為什麼還會被稱之為超集合(SuperSet)呢?」

原因很簡單,因為 TypeScript 具備了其他 JavaScript 所沒有的特性,例如…

  • 型別系統
  • 介面
  • 裝飾器
  • 泛型
    …等等

正因為 TypeScript 具備了其他 JavaScript 所沒有的特性,所以 TypeScript 才會被稱之為超集合(SuperSet),所以概念圖如下:

TypeScript 超集合

因此如果你要把 TypeScript 當作一個擴充版的 JavaScript 來看待,那也沒有錯,因為 TypeScript 確實是在 JavaScript 的基礎上,擴充了許多 JavaScript 所沒有的特性,所以 TypeScript 也可以被稱之為 JavaScript 的擴充版(只是比較常見人家稱之為超集合)。

TypeScript 特性

接下來會簡單聊一下最具代表的 TypeScript 特性,也就是型別系統,但這一篇並不是要教學如何撰寫 TypeScript,因此並不會將所有的 TypeScript 特性都介紹過一遍,而是讓你知道為什麼 TypeScript 會被稱之為超集合(SuperSet),以及為什麼 TypeScript 變成了許多開發者的必備技能之一。

首先我們在普通開發時,可能會宣告一個變數,然後給予一個值,如下:

1
const myName = 'Ray';

而 TypeScript 在宣告時,你可以針對該變數進行型別註記(Type Annotation),如下:

1
const myName: string = 'Ray';

這樣子當我們在閱讀程式碼時,就可以知道該變數的型別是什麼,而不需要再去看該變數的值是什麼,但其實這邊有一個小細節,就是這個變數不寫型別註記的話,TypeScript 會自動推論(Type Inference)出該變數的型別,所以你可以不用寫型別註記,但是如果你寫了型別註記,那 TypeScript 會以你寫的型別註記為主,如下:

1
const myName = 'Ray'; // TypeScript 會自動推論出 name 的型別為 string
1
const myName: string = 'Ray'; // TypeScript 會以你寫的型別註記為主

這兩種差異其實並不大,只是前者依賴著 TypeScript Compiler 自動推論,而後者則是你自己寫型別註記,這時候你應該會好奇自己寫型別註記跟 TypeScript 自動推論有什麼差別吧?其實差別不大,只是當你在撰寫大型專案時,你會發現自動推論會有一些問題,例如:

1
2
3
let myName = 'Ray'; // TypeScript 會自動推論出 name 的型別為 string

myName = 123; // 這時候你會發現 TypeScript 會報錯,因為 TypeScript 自動推論出 myName 的型別為 string,所以當你給予 number 時,就會報錯

但是你的需求會需要 myName 可以是 string 或是 number,這時候你就必須要自己寫型別註記,如下:

1
2
3
let myName: string | number = 'Ray'; // 這時候你就可以給予 string 或是 number 了

myName = 123; // 這時候就不會報錯了

雖然讓 TypeScript 自動推論出型別很方便,也可以讓程式碼整體看起來比較簡潔,但是對於比較複雜的需求還是會建議你自己寫型別註記,這樣子可以讓你的程式碼更加的清楚,也可以避免一些不必要的錯誤,所以通常型別推論是比較常見於一些簡單的需求,例如…你可以很輕鬆自己從眼睛就判斷出來的型別,或是型別非常明確的狀況。

前面我們簡單的介紹了 TypeScript 最著名的型別系統,你可能就會想說

「為什麼寫前端會需要型別系統呢?」

其實這個算是一個 JavaScript 語言特性的問題,因為 JavaScript 是一個動態型別(Dynamic Type)的語言,所以你在撰寫時,並不需要去宣告變數的型別,也不需要去宣告函式的回傳值型別,正因為這種特性的關係,我們很難掌握 JavaScript 程式碼的型別,通昂你必須要去執行程式碼,才能知道該變數的型別是什麼,所以讓我們來看一個簡單的加減例子:

1
2
3
function add(a, b) {
return a + b;
}

如果你是一個比較有經驗的開發者,可能一看就知道這個函式的回傳值型別是 number,**只要你傳入的參數是 number,那回傳值一定也是 number**,但是如果你是一個新手開發者,你可能就會被 JavaScript 的動態型別陰一輪,什麼意思呢?

假設我們一開始都是這樣呼叫:

1
add(1, 2);

然後直到某一天我們腦袋可能啪袋一下,然後就不小心手指跟大腦沒配合好變成這樣呼叫:

1
add(1, '2');

你會發現回傳的不再是 number,而是 string,然後你就必須要被迫去看看函式裡面的程式碼,才能知道為什麼,接著你可能為了避免這種情況,所以你就會這樣寫:

1
2
3
4
5
6
7
function add(a, b) {
if (typeof a !== 'number' || typeof b !== 'number') {
throw new Error('a 或 b 不是 number');
}

return a + b;
}

這做法並沒有錯,但是你會發現你的程式碼變得非常冗長,而且你還是必須要先執行程式碼,才能知道這一段程式碼到底有沒有問題。

如果是使用 TypeScript 呢?你可以這樣寫:

1
2
3
function add(a: number, b: number): number {
return a + b;
}

接下來當你只要傳入錯誤的型別時 TypeScript 就會報錯,如下:

1
add(1, '2'); // 會噴「類型 'string' 的引數不可指派給類型 'number' 的參數。」,因為 TypeScript 會檢查你傳入的參數是否符合你所寫的型別註記

透過 TypeScript 的型別系統,你可以在開發階段就知道程式碼是否有問題,而不需要等到執行時才知道。

除此之外,正確的推斷型別也有助於開發者打錯字或是選錯方法,例如:

1
const arr: number[] = [1, 2, 3];

接著當你下拉時,你會發現 TypeScript 會自動提示你可以使用的方法,如下:

自動提示

因此使用 TypeScript 除了解決 JavaScript 的動態型別問題外,還可以提升開發者的開發體驗。

何時該用 TypeScript 以及注意事項

前面我們已經簡單的介紹了 TypeScript 的特性,以及為什麼 TypeScript 會被稱之為超集合(SuperSet),那麼接下來就讓我們來聊聊什麼時候該使用 TypeScript 吧!

兩人(含)以上的專案

基本上我個人認為 TypeScript 比較適合在兩人(含)以上的專案中使用,當然也不代表只有一個人的專案就不適合使用 TypeScript,只是我個人認為當專案人數越多時,就越需要一個型別系統來幫助開發者,因為當專案人數越多時,就越容易出現一些不必要的錯誤。

假設你今天是兩個人合作,然後 A 寫了以下程式碼作為共用呼叫以及使用

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
const myMoney = (storage) => {
let money = storage;
return (price) => {
return {
nowMoney: () => money,
count: (price) => {
if (money < price) return `餘額不足,目前餘額: ${money} $`;
if (money > 0) {
money -= price;
return money;
}
return `餘額扣除失敗,目前餘額: ${money} $`;
}
};
};
};

然後 B 在使用時,就必須要先看 A 的程式碼,才能知道該函式的回傳值型別是什麼,以及該函式的參數型別是什麼,但是如果使用 TypeScript 的話你就沒有這些問題,因為 TypeScript 會自動提示你該函式的回傳值型別是什麼,以及該函式的參數型別是什麼,只需要將該函式的型別註記寫好,如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
type MoneyManager = {
nowMoney: () => number;
count: (price: number) => number | string;
};

const myMoney = (storage: number): (price: number) => MoneyManager => {
let money = storage;
return (price: number): MoneyManager => {
return {
nowMoney: (): number => money,
count: (price: number): number | string => {
if (money < price) return `餘額不足,目前餘額: ${money} $`;
if (money > 0) {
money -= price;
return money;
}
return `餘額扣除失敗,目前餘額: ${money} $`;
}
};
};
};

這樣子當 B 在使用時,就可以很清楚的知道該函式的回傳值型別是什麼,以及該函式的參數型別是什麼,而不需要去看 A 的程式碼,這樣子就可以避免一些不必要的錯誤,而且也可以讓程式碼更加的清楚。

希望專案有更好的可維護性

前面的許多範例,我們都可以看到透過 TypeScript 的型別系統,可以讓程式碼更加的清楚,而且也可以避免一些不必要的錯誤,所以如果你希望專案有更好的可維護性,那麼 TypeScript 也是一個不錯的選擇。

只是我個人會建議在專案有寫測試的情況下,才使用 TypeScript,因為 TypeScript 並不會幫你檢查邏輯上的錯誤,如果你盲目的將專案轉換成 TypeScript,那麼你為了解決這些 TypeScript 錯誤時,可能會修改到原本的邏輯,所以我個人會建議在專案有寫測試的情況下,才使用 TypeScript。

例如,原本有一段程式碼如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
function calculateTotalPrice(cartItems) {
return cartItems.reduce((total, item) => {
return total + item.price * item.quantity;
}, 0);
}

// 測試資料
const testCartItems = [
{ id: 1, name: 'Book', price: 10, quantity: 3 },
{ id: 2, name: 'Pen', price: 5, quantity: 2 },
];

console.log(calculateTotalPrice(testCartItems)); // 應該要印出 40

接著你想要將這段程式碼轉換成 TypeScript,但是你會發現 TypeScript 會報錯,因為 TypeScript 會自動推論出 cartItems 的型別為 any[],所以你必須要自己寫型別註記,如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
type CartItem = {
id: number;
name: string;
price: number;
quantity: number;
};

function calculateTotalPrice(cartItems: CartItem[]): number {
return cartItems.reduce((total, item) => {
return total + item.price * item.quantity;
}, 0);
}

// 測試範例
const testCartItems: CartItem[] = [
{ id: 1, name: 'Book', price: 10, quantity: 3 },
{ id: 2, name: 'Pen', price: 5, quantity: 2 },
];

console.log(calculateTotalPrice(testCartItems)); // 應該要印出 40

轉換的過程中,你的程式碼確實透過 TypeScript 提高了可維護性,但是如果你不小心在重構過程中改變了邏輯,例如將 item.price * item.quantity 改成了 item.price + item.quantity,這時候如果你沒有撰寫測試的話,你就很難發現這個錯誤,因為 TypeScript 並不會幫你檢查邏輯上的錯誤,所以我個人會建議在專案有寫測試的情況下,才使用 TypeScript。

因為你也很難保證你在轉換過程中,不會不小心粗手指,所以我個人會建議在專案有寫測試的情況下,才引入 TypeScript。

團隊成員都有 TypeScript 基礎

這是一個我認為滿重要的事情,有些 Team Leader(團隊負責人) 看到人家宣傳 TypeScript 有多香之後,就想要將專案轉換成 TypeScript,但是他們忘記了一件事情,就是團隊成員是否都有 TypeScript 基礎,如果團隊成員都沒有 TypeScript 基礎,那麼你就必須要花一些時間來學習 TypeScript。

如果是專案時程不受任何影響的話,那麼我認為還是可以引入 TypeScript,但是如果是專案時程很趕的話,那麼我建議還是不要引入 TypeScript,因為你們必須要花一些時間來學習 TypeScript。

「所以我們就只能放棄 TypeScript 嗎?」

話不是這樣說的,只是我認為在嘗試使用 TypeScript 之前,你們可以嘗試撰寫 JSDoc,JSDoc 是一種註解語法,可以讓你在撰寫 JavaScript 時,可以寫上一些註解,讓你的程式碼更加的清楚,而且也可以讓你的程式碼有一些型別檢查,例如:

1
2
3
4
5
6
7
8
9
/**
* 計算總價
* @param {number} a
* @param {number} b
* @returns {number}
*/
function add(a, b) {
return a + b;
}

習慣撰寫 JSDoc 對於你們未來轉換 TypeScript 會有一些幫助,因為概念上 TypeScript 的型別註記跟 JSDoc 的註解有點類似,這樣子的陣痛期也是相對比較少的,所以我建議在專案時程很趕的情況下,可以先嘗試撰寫 JSDoc,等到專案時程不趕時,再來嘗試轉換 TypeScript。

Note
這邊我也提供一篇我先前寫的文章「沒辦法用 TypeScript?別擔心,你還可以寫 JSDoc 標注類型」。

TypeScript 並非萬靈藥

有件事情我必須要先跟你說,並不是代表使用 TypeScript 就不會發生 JavaScript 的一些隱含雷點,你必須熟記一個原因

「TypeScript 本質上是 JavaScript 的超集合(SuperSet),因此編譯出來的程式碼還是 JavaScript。」

千萬不要認為使用了 TypeScript 就可以避掉 JavaScript 的一些隱含雷點,例如…你寫了一個表單功能,然後你都有針對這些表單物件寫上型別註記,但實際上編譯出來後,還是 JavaScript,底下是一個簡單的範例:

1
2
3
4
5
6
7
8
9
10
11
type Form = {
name: string;
email: string;
phone: string;
};

const form: Form = {
name: '',
email: '',
phone: '',
};

實際上編譯出來還是 JavaScript,如下:

1
2
3
4
5
6
"use strict";
const form = {
name: '',
email: '',
phone: '',
};

See the Pen typescript-example by Ray (@hsiangfeng) on CodePen.

因此你本身的 JavaScript 程式碼邏輯還是有可能出現一些隱含雷點,該做的預防你還是閃不掉,千萬不要認為使用了 TypeScript 就解決了所有問題,因為 TypeScript 本質上還是 JavaScript。

TypeScript 共同規範

不得不說 TypeScript 的型別機制確實讓我們減少了許多開發雷點,也提升了開發體驗(Developer Experience,又稱 DX),我曾經看過有開發者用著 TypeScript 卻全部都使用 any 來作為註記,但是難道我們就完全不能使用 any 嗎?

不對,而是要依據你的需求來決定是否使用 any,如果你能夠知道該變數的型別,那麼就不要使用 any,但是如果你無法知道該變數的型別,那麼你就可以使用 any

例如…你有一個函式,但是你不知道該函式的回傳值型別,這時候你就可以使用 any,如下:

1
2
3
function getSomething(): any {
// ...
}

但把握一個原則,就是盡量不要使用 any,因為你使用 any 就代表你放棄了 TypeScript 的型別檢查,所以盡量不要使用 any

要這樣子寫不如就改叫 AnyScript

結論

以上差不多就是我自己對於 TypeScript 以及為什麼要用的看法了,希望可以讓初次接觸 TypeScript 的開發者,可以對 TypeScript 有一些初步的認識,以及為什麼要用 TypeScript,而非盲目跟風的導入。

Liker 讚賞

這篇文章如果對你有幫助,你可以花 30 秒登入 LikeCoin 並點擊下方拍手按鈕(最多五下)免費支持與牡蠣鼓勵我。
或者你可以也可以請我「喝一杯咖啡(Donate)」。

Buy Me A Coffee Buy Me A Coffee

Google AD

撰寫一篇文章其實真的很花時間,如果你願意「關閉 Adblock (廣告阻擋器)」來支持我的話,我會非常感謝你 ヽ(・∀・)ノ