
英文 | https://betterprogramming.pub/7-typescript-common-mistakes-to-avoid-581c30e514d6
翻譯 | 楊小二
自 2012 年 10 月首次出現以來,TypeScript 獲得了廣泛的關注,它已成為 Web 開發領域真正的遊戲規則改變者。盡管如此,有些人一直對使用它持懷疑态度。
将 TypeScript 添加到項目時,開發人員應該接受它而不是反對它,這一點很重要。
這可能會使你沮喪,以及在開發中TypeScript 會成為開發過程中的瓶頸。
但是,如果使用得當,擁有可讀且易于維護的代碼庫就變得至關重要。
它具有強大的功能,例如映射類型、重載、類型推斷、可選類型等,并且随着增量更新,這些功能每天都在變得更好。
為什麼有些人會覺得 TypeScript 正在損害他們的生産力?我們怎樣才能防止這種不好情況的發生?是否有一些我們可以采用的最佳實踐做法?
在這裡,我們将讨論使用 TypeScript 時最常見的錯誤。通過不迷戀這些常見的問題,我們将看到我們的生産力和代碼可維護性的提高。
現在開始吧。
1、不啟用嚴格模式
如果沒有打開 TypeScript 嚴格模式,類型可能會過于松散,這将使我們的代碼庫類型安全性降低。它會給人錯誤的印象,因為有些人認為通過添加 TypeScript,所有TypeScript問題都會自動修複。
以後,我們将成為這些類型的受害者。我們最終可能會用更新檔修複它們,而不是修複根本原因。這可能會導緻你認為該工具做得不好。
我們如何啟用嚴格模式?它通過在 tsconfig.json 檔案中将 strict 參數設定為 true 來啟用,如下所示:
{
...
"compilerOptions": {
"strict": true,
...
},
...
}
啟用strict模式将在鈎子下啟用:
noImplicitAny:此标志可防止我們使用推斷的 any 公開合約。如果我們不指定類型并且無法推斷,則預設為any。
noImplicitThis:它将防止 this 關鍵字的不必要的不安全用法。防止不需要的行為将使我們免于一些調試麻煩,如下所示:
class Book {
pages: number;
constructor(totalPages: number) {
this.pages = totalPages;
}
isLastPageFunction() {
return function (currentPage: number) {
// ❌ 'this' here implicitly has type 'any' because it does not have a type annotation.
return this.pages === currentPage;
}
}
}
alwaysStrict:這将確定在我們所有轉換後的 JavaScript 檔案中發出 use strict ,但編譯器除外。這将提示 JavaScript 引擎代碼應該在嚴格模式下執行。
strictBindCallApply:這将確定我們使用正确的參數調用 call 、 bind 和 apply 函數。讓我們看一個例子:
const logNumber = (x: number) => {
console.log(`number ${x} logged!`)
}
// ✅ works fine
logNumber.call(undefined, 10);
// ❌ error: Argument of type 'string' is not assignable to parameter of type 'number'.ts(2345)
logNumber.call(undefined, "10");
strictNullChecks:如果此标志關閉,則編譯器會有效地忽略 undefined、null 和 false。松散的輸入可能會導緻運作時出現意外錯誤。讓我們看一個例子:
interface Person {
name: string | undefined;
age: number;
}
const x: Person = { name: 'Max', age: 3 };
// ❌ Works with strictNullChecks off, which is lax
console.log(x.name.toLowerCase());
// ✅ Fails with strictNullChecks on as x.name could be undefined
console.log(x.name.toLowerCase());
strictFunctionTypes:啟用此标志可確定更徹底地檢查函數參數。
strictPropertyInitialization:當設定為 true 時,這将強制我們在構造函數中設定所有屬性值。
正如所見,TypeScript 的嚴格變量是上述所有标志的簡寫。我們可以通過使用嚴格或逐漸啟用它們來啟用它們。
更嚴格的類型将幫助我們在編譯時捕獲更多錯誤。
2、重新聲明接口
在鍵入元件接口時,通常需要具有相同類型的一些不同接口變體。這些可以在一兩個參數中變化。一個常見的錯誤是手動重新定義這些變體。這将導緻:
- 不必要的樣闆。
- 需要多次更改。如果一個屬性在一個地方發生變化,則需要将該更改傳播到多個檔案。
很久以前,TypeScript 釋出了一個旨在解決此目的的功能:映射類型。它們讓我們可以根據我們定義的一些規則,在現有類型的基礎上建立新類型。這确實會導緻更具可讀性和聲明性的代碼庫。
讓我們看一個例子:
interface Book {
author?: string;
numPages: number;
price: number;
}
// ✅ Article is a Book without a Page
type Article = Omit<Book, 'numPages'>;
// ✅ We might need a readonly verison of the Book Type
type ReadonlyBook = Readonly<Book>;
// ✅ A Book that must have an author
type NonAnonymousBook = Omit<Book, 'author'> & Required<Pick<Book, 'author'>>;
在上面的代碼中,我們保留了一個單一的事實來源:Book 實體。它的所有變體都使用映射類型功能來表達,這大大減少了對代碼進行類型化和維護的成本。
映射類型也可以應用于聯合,如下所示:
type animals = 'bird' | 'cat' | 'crocodile';
type mamals = Exclude<animals, 'crocodile'>;
// 'bird' | 'cat'
TypeScript 附帶以下映射類型:Omit、Partial、Readonly、Exclude、Extract、NonNullable、ReturnType。
我們可以建立自己的實用程式并在我們的代碼庫中重用它們。
3、不依賴類型推斷
TypeScript 推理是這種程式設計語言最強大的工具之一。它為我們完成所有工作。我們隻需要確定在盡可能少的幹預下将所有部分加在一起。
實作這一目标的一個關鍵操作符是 typeof。它是一個類似于 JavaScript 的運算符。它不會傳回 JavaScript 類型,而是傳回 TypeScript 類型。使用這個操作數可以避免我們重新聲明相同的類型。
讓我們通過一個例子來看看:
const addNumber = (a: number, b: number) => a + b;
// ❌ you are hardcoding the type `number` instead of relying on what the function returns
const processResult = (result: number) => console.log(result);
processResult(addNumber(1, 1));
// ✅ result will be whatever the return type of addNumber function
// no need for us to redefine it
const processResult = (result: ReturnType<typeof addNumber>) => console.log(result);
processResult(addNumber(1, 1));
在上面的代碼中,注意結果參數類型。最好依賴 ReturnType<typeof addNumber> 而不是添加數字類型。通過對數字類型進行寫死,我們完成了編譯器的工作。
最好使用适當的文法來表達我們的類型。TypeScript 将為我們完成繁重的工作。
讓我們看一個虛拟示例:
// ❌ Sometimes for one of objects it is not necessary to define
// interfaces for it
interface Book {
name: string,
author: string
}
const book: Book = {
name: 'For whom the bell tolls',
author: 'Hemingway'
}
const printBook = (bookInstance: Book) => console.log(bookInstance)
請注意,Book 接口用于特定場景,甚至不需要建立接口。
通過依賴 TypeScript 的推理,代碼變得不那麼雜亂,更易于閱讀。下面是一個例子:
// ✅ For simple scenarios we can rely on type inference
const book = {
name: 'For whom the bell tolls',
author: 'Hemingway'
}
const printBook = (bookInstance: typeof book) => console.log(bookInstance)
TypeScript 甚至有 infer 運算符,它可以與 Mapped Types 結合使用以從另一個類型中提取一個類型。
const array: number[] = [1,2,3,4];
// ✅ type X will be number
type X = typeof array extends (infer U)[] ? U : never;
在上面的例子中,我們可以看到如何提取數組的類型。
4、不正确的使用 Overloading
TypeScript 本身支援重載。這很好,因為它可以提高我們的可讀性。但是,它不同于其他類型的重載語言。
在某些情況下,它可能會使我們的代碼更加複雜和冗長。為了防止這種情況發生,我們需要牢記兩條規則:
1. 避免編寫多個僅尾随參數不同的重載
// ❌ instead of this
interface Example {
foo(one: number): number;
foo(one: number, two: number): number;
foo(one: number, two: number, three: number): number;
}
// ❎ do this
interface Example {
foo(one?: number, two?: number, three?: number): number;
}
你可以看到兩個接口是如何相等的,但第一個比第二個更冗長。在這種情況下最好使用可選參數。
2. 避免僅在一種參數類型中編寫因類型不同而不同的重載
// ❌ instead of this
interface Example {
foo(one: number): number;
foo(one: number | string): number;
}
// ❎ do this
interface Example {
foo(one: number | string): number;
}
與前面的示例一樣,第一個界面變得非常冗長。最好使用聯合來代替。
5、使用函數類型
TypeScript 附帶 Function 類型。這就像使用 any 關鍵字但僅用于函數。遺憾的是,啟用嚴格模式不會阻止我們使用它。
這裡有一點關于函數類型:
- 它接受任意數量和類型的參數。
- 傳回類型始終為 any。
讓我們看一個例子:
// ❌ Avoid, parameters types and length are unknown. Return type is any
const onSubmit = (callback: Function) => callback(1, 2, 3);
// ✅ Preferred, the arguments and return type of callback is now clear
const onSubmit = (callback: () => Promise<unknown>) => callback();
在上面的代碼中,通過使用顯式函數定義,我們的回調函數更具可讀性和類型安全性。
6、依賴第三方實作不變性
在使用函數式程式設計範式時,TypeScript 可以提供很大幫助。它提供了所有必要的工具來確定我們不會改變我們的對象。我們不需要在我們的代碼庫中添加像 ImmutableJS這樣的笨重的庫。
讓我們通過以下示例來看看我們可以使用的一些工具:
// ✅ declare properties as readonly
interface Person {
readonly name: string;
readonly age: number;
}
// ✅ implicitely declaring a readonly arrays
const x = [1,2,3,4,5] as const;
// ✅ explicitely declaring a readonly array
const y: ReadonlyArray<{ x: number, y: number}> = [ {x: 1, y: 1}]
interface Address {
street: string;
city: string;
}
// ✅ converting all the type properties to readonly
type ReadonlyAddress = Readonly<Address>;
正如你從上面的例子中看到的,我們有很多工具來保護我們的對象免于變異。
通過使用内置功能,我們将保持我們的 bundle light 和我們的類型一緻。
7、不了解 infer/never 關鍵字
infer 和 never 關鍵字很友善,可以在許多情況下提供幫助,例如:
推斷
使用 infer 關鍵字就像告訴 TypeScript,“我想把你在這個位置推斷出來的任何東西配置設定給一個新的類型變量。”
我們來看一個例子:
const array: number[] = [1,2,3,4];
type X = typeof array extends (infer U)[] ? U : never;
在上面的代碼中,作為array extends infer U[],X變量将等于 a Number。
never
該never類型表示值是不會發生的類型。
我們來看一個例子:
interface HttpResponse<T, V> {
data: T;
included?: V;
}
type StringHttpResponse = HttpResponse<string, never>;
// ❌ included prop is not assignable
const fails: StringHttpResponse = {
data: 'test',
included: {}
// ^^^^^
// Type '{}' is not assignable to type 'never'
}
// ✅ included is not assigned
const works: StringHttpResponse = {
data: 'test',
}
在上面的代碼中,我們可以使用 never 類型來表示我們不希望某個屬性是可指派的。
我們可以将 Omit Mapped 類型用于相同的目的:
type StringHttpResponse = Omit<HttpResponse<string, unkown>, 'included'>;
但是,你可以看到它的缺點。它更冗長。如果你檢查 Omit 的内部結構,它會使用 Exclude,而後者又使用 never 類型。
通過依賴 infer 和 never 關鍵字,我們省去了複制任何類型的麻煩,并更好地表達我們的接口。
總結
這些指南易于遵循,旨在幫助你接受 TypeScript,而不是與之抗争。TypeScript 旨在幫助你建構更好的代碼庫,而不是妨礙你。
通過應用這些簡單的技巧,你将擁有一個更好、更簡潔且易于維護的代碼庫。
我們是否遺漏了你項目中經常發生的任何常見錯誤?請在評論中與我分享它們。
感謝你的閱讀。
學習更多技能
請點選下方公衆号