要改掉的 10 種 TypeScript 壞習慣

語言: CN / TW / HK

在過去的幾年中,TypeScript 和 JavaScript 一直在穩步發展,而我們在過去的幾十年中養成的一些程式設計習慣也變得過時了。其中有一些習慣可能從來就沒有什麼意義可言。這篇文章就來談一談我們大家都應該改掉的 10 個習慣。

接下來我們就來一個個看示例吧!請注意,每個小節中“應該怎麼做”這部分只糾正了前文提到的問題,實際情況中可能還要其他需要注意的程式碼風味。
1. 不使用 strict 模式
具體是什麼意思
在沒有啟用 strict 模式的情況下使用 tsconfig.json。
{
"compilerOptions": {
"target": "ES2015",
"module": "commonjs"
}
}
應該怎麼做
只需啟用 strict 模式即可:
{
"compilerOptions": {
"target": "ES2015",
"module": "commonjs",
"strict": true
}
}
我們為什麼養成了這樣的習慣

在現有程式碼庫中引入更嚴格的規則需要花費時間。

為什麼應該糾正它

更嚴格的規則會讓程式碼在將來更容易更改,因此用來修復程式碼的時間會在將來使用儲存庫時獲得超額回報。

2. 用||定義預設值
具體是什麼意思
對可選值用||回退:
function createBlogPost (text: string, author: string, date?: Date) {
return {
text: text,
author: author,
date: date || new Date()
}
}
應該怎麼做
使用新的?? 運算子,或者更好的是,在引數級別正確定義回退。
function createBlogPost (text: string, author: string, date: Date = new Date())
return {
text: text,
author: author,
date: date
}
}
我們為什麼養成了這樣的習慣

這個?? 運算子是去年才引入的,所以在長函式中間使用值時,可能很難習慣將其設定為引數預設值。

為什麼應該糾正它

與||不同,?? 僅對 null 或 undefined 回退,而不對所有虛假值回退。另外,如果你的函式太長而無法在開始時定義預設值,那麼將它們拆分可能是個好主意。

3. 使用 any 型別
具體是什麼意思
當你不確定結構時,將 any 用於資料。
async function loadProducts(): Promise<Product[]> {
const response = await fetch('https://api.mysite.com/products')
const products: any = await response.json()
return products
}
應該怎麼做
幾乎在每種情況下,當你給什麼東西定義 any 型別時,實際上應該給它定 unknown 型別。
async function loadProducts(): Promise<Product[]> {
const response = await fetch('https://api.mysite.com/products')
const products: unknown = await response.json()
return products as Product[]
}
我們為什麼養成了這樣的習慣

any 很方便,因為它基本上會禁用所有型別檢查。通常,即使在正式型別化中也會用到 any(例如,上面示例中的 response.json() 被 TypeScript 團隊定義為 Promise )。

為什麼應該糾正它

它基本上會禁用所有型別檢查。通過 any 傳入的任何內容將完全放棄任何型別檢查。這導致系統難以捕獲錯誤,因為僅當我們對型別結構的假設與執行時程式碼相關時,程式碼才會失敗。

4. valasSomeType
具體是什麼意思
強制告知編譯器一個它無法推斷的型別。
async function loadProducts(): Promise<Product[]> {
const response = await fetch('https://api.mysite.com/products')
const products: unknown = await response.json()
return products as Product[]
}
應該怎麼做
這就是 type guard 的用武之地。
function isArrayOfProducts (obj: unknown): obj is Product[] {
return Array.isArray(obj) && obj.every(isProduct)
}
function isProduct (obj: unknown): obj is Product {
return obj != null
&& typeof (obj as Product).id === 'string'
}
async function loadProducts(): Promise<Product[]> {
const response = await fetch('https://api.mysite.com/products')
const products: unknown = await response.json()
if (!isArrayOfProducts(products)) {
throw new TypeError('Received malformed products API response')
}
return products
}
我們為什麼養成了這樣的習慣

從 JavaScript 轉換為 TypeScript 時,現有的程式碼庫通常會對 TypeScript 編譯器無法自動推斷出的型別進行假設。在這些情況下,簡單地用一個 as SomeOtherType 可以加快轉換速度,而不必放鬆 tsconfig 中的設定。

為什麼應該糾正它

即使斷言現在可以儲存,當有人將程式碼移植到其他位置時這種情況也可能會改變。type guard 將確保所有檢查都是明確的。

5. 測試中的 as any
具體是什麼意思
編寫測試時建立不完整的替身。
interface User {
id: string
firstName: string
lastName: string
email: string
}
test('createEmailText returns text that greats the user by first name', () => {
const user: User = {
firstName: 'John'
} as any

expect(createEmailText(user)).toContain(user.firstName)
}
應該怎麼做
如果你需要模擬測試資料,請將模擬邏輯移到要模擬的物件旁邊,並使其可重用。
interface User {
id: string
firstName: string
lastName: string
email: string
}
class MockUser implements User {
id = 'id'
firstName = 'John'
lastName = 'Doe'
email = '[email protected]'
}
test('createEmailText returns text that greats the user by first name', () => {
const user = new MockUser()
expect(createEmailText(user)).toContain(user.firstName)
}
我們為什麼養成了這樣的習慣

在尚不具備廣泛的測試覆蓋範圍的程式碼庫中編寫測試時,通常會存在複雜的大資料結構,但是要測試的特定功能只用到其中的一部分。短期內不必擔心其他屬性。

為什麼應該糾正它

放棄建立模擬會讓我們付出代價,因為遲早會有一個屬性更改會要求我們在所有測試中做更改,而不是一處改完全部生效。同樣,在某些情況下,被測程式碼會依賴於我們之前認為不重要的屬性,然後我們就需要更新針對該功能的所有測試。

6. 可選屬性
具體是什麼意思
一些屬性有時存在,有時不存在,就將它們標為可選。
interface Product {
id: string
type: 'digital' | 'physical'
weightInKg?: number
sizeInMb?: number
}
應該怎麼做
明確建模哪些組合存在,哪些不存在。
interface Product {
id: string
type: 'digital' | 'physical'
}
interface DigitalProduct extends Product {
type: 'digital'
sizeInMb: number
}
interface PhysicalProduct extends Product {
type: 'physical'
weightInKg: number
}
我們為什麼養成了這樣的習慣

將屬性標記為可選而不是拆分型別做起來會更容易,並且生成的程式碼更少。它還需要對正在構建的產品有更深入的瞭解,並且如果對產品的假設發生更改,可能會限制程式碼的使用。

為什麼應該糾正它

型別系統的最大好處是它們可以用編譯時檢查代替執行時檢查。通過更顯式的型別化,可以對可能被忽略的錯誤進行編譯時檢查,例如確保每個 DigitalProduct 都有一個 sizeInMb。

7. 單字母泛型
具體是什麼意思
用一個字母命名一個泛型。
function head<T> (arr: T[]): T | undefined {
return arr[0]
}
應該怎麼做
提供完整的描述性型別名稱。
function head<Element> (arr: Element[]): Element | undefined {
return arr[0]
}
我們為什麼養成了這樣的習慣

我猜想這個習慣越來越常見,因為即使是官方文件也在使用一個字母的名稱:

https://www.typescriptlang.org/docs/handbook/generics.html

它的型別化速度也更快,並且不需要花很多時間來寫一個全名。

為什麼應該糾正它

泛型型別變數是變數,就像其他變數一樣。當 IDE 開始向我們展示變數的技術性時,我們已經放棄了以它們的名稱描述變數技術性的想法。例如現在我們只寫 const name='Daniel',而不是 const strName='Daniel'。另外,一個字母的變數名通常不容易看懂,因為不看宣告就很難理解它們的含義。

8. 非布林布林檢查
具體是什麼意思
將一個值直接傳遞給 if 語句來檢查是否定義了這個值。
function createNewMessagesResponse (countOfNewMessages?: number) {
if (countOfNewMessages) {
return `You have ${countOfNewMessages} new messages`
}
return 'Error: Could not retrieve number of new messages'
}
應該怎麼做
明確檢查我們關心的狀況。
function createNewMessagesResponse (countOfNewMessages?: number) {
if (countOfNewMessages !== undefined) {
return `You have ${countOfNewMessages} new messages`
}
return 'Error: Could not retrieve number of new messages'
}
我們為什麼養成了這樣的習慣

編寫簡短的檢檢視起來更加簡潔,這樣我們就可以不去思考我們實際想要檢查的內容。

為什麼應該糾正它

也許我們應該考慮一下我們實際要檢查的內容。例如,上面的示例處理了 countOfNewMessages 為 0 的不同情況。

9. BangBang 運算子
具體是什麼意思
將一個非布林值轉換為布林值。
function createNewMessagesResponse (countOfNewMessages?: number) {
if (!!countOfNewMessages) {
return `You have ${countOfNewMessages} new messages`
}
return 'Error: Could not retrieve number of new messages'
}
應該怎麼做
明確檢查我們關心的狀況。
function createNewMessagesResponse (countOfNewMessages?: number) {
if (countOfNewMessages !== undefined) {
return `You have ${countOfNewMessages} new messages`
}
return 'Error: Could not retrieve number of new messages'
}
我們為什麼養成了這樣的習慣

對於一些人來說,理解!! 就像是 JavaScript 世界的入門儀式。它看起來簡短而簡潔,如果你已經習慣了用它,那麼你就會知道它的含義。這是將任何值轉換為布林值的捷徑。尤其是在程式碼庫中,當虛假值(例如 null、undefined 和'')之間沒有明確的語義分隔時。

為什麼應該糾正它

像許多快捷方式和入門儀式一樣,使用!! 會混淆程式碼的真實含義。這使得程式碼庫對於新開發人員來說用起來更麻煩,不管新人是程式碼新手還是說只是 JavaScript 新手都一樣。引入細微的錯誤也很容易。用!! 時。“非布林布林檢查”的 countOfNewMessages 為 0 的問題仍然存在。

10. != null
具體是什麼意思
bang bang 運算子的小姐妹!= null 允許我們同時檢查 null 和 undefined。
function createNewMessagesResponse (countOfNewMessages?: number) {
if (countOfNewMessages != null) {
return `You have ${countOfNewMessages} new messages`
}
return 'Error: Could not retrieve number of new messages'
}
應該怎麼做
明確檢查我們關心的狀況。
function createNewMessagesResponse (countOfNewMessages?: number) {
if (countOfNewMessages !== undefined) {
return `You have ${countOfNewMessages} new messages`
}
return 'Error: Could not retrieve number of new messages'
}
我們為什麼養成了這樣的習慣

如果到了這一步,就意味著你的程式碼庫和技能已經處於良好狀態。甚至大多數在!= 上強制使用!== 的 linting 規則集都可以豁免!= null。如果程式碼庫在 null 和 undefined 之間沒有明顯的區別,則!= null 有助於簡化對這兩種可能性的檢查。

為什麼應該糾正它

儘管 null 值在 JavaScript 的早期很麻煩,但在 TypeScript 的 strict 模式下,它們卻可以成為這種語言工具帶中的寶貴成員。我看到的一個常見模式是將 null 值定義為不存在的事物,而 undefined 定義為不未知的事物,例如 user.firstName === null 可能意味著使用者實際上沒有名字,而 user.firstName === undefined 只是意味著我們還沒有要求該使用者提供名字(而 user.firstName === ''會意味著名字是''——現實中存在的名字之多會讓你大吃一驚)。

本文分享自微信公眾號 - 前端研究所(WEBqdyjs)。
如有侵權,請聯絡 [email protected] 刪除。
本文參與“OSC源創計劃”,歡迎正在閱讀的你也加入,一起分享。

分享到: