7個Typescript常見錯誤需要你注意避免發(fā)生

英文 | https://betterprogramming.pub/7-typescript-common-mistakes-to-avoid-581c30e514d6
翻譯 | 楊小二
{..."compilerOptions": {"strict": true,...},...}
啟用strict模式將在鉤子下啟用:
noImplicitAny:此標志可防止我們使用推斷的 any 公開合約。如果我們不指定類型并且無法推斷,則默認為any。
noImplicitThis:它將防止 this 關鍵字的不必要的不安全用法。防止不需要的行為將使我們免于一些調(diào)試麻煩,如下所示:
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:這將確保在我們所有轉(zhuǎn)換后的 JavaScript 文件中發(fā)出 use strict ,但編譯器除外。這將提示 JavaScript 引擎代碼應該在嚴格模式下執(zhí)行。
strictBindCallApply:這將確保我們使用正確的參數(shù)調(diào)用 call 、 bind 和 apply 函數(shù)。讓我們看一個例子:
const logNumber = (x: number) => {console.log(`number ${x} logged!`)}// ? works finelogNumber.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。松散的輸入可能會導致運行時出現(xiàn)意外錯誤。讓我們看一個例子:
interface Person {name: string | undefined;age: number;}const x: Person = { name: 'Max', age: 3 };// ? Works with strictNullChecks off, which is laxconsole.log(x.name.toLowerCase());// ? Fails with strictNullChecks on as x.name could be undefinedconsole.log(x.name.toLowerCase());
strictFunctionTypes:啟用此標志可確保更徹底地檢查函數(shù)參數(shù)。
strictPropertyInitialization:當設置為 true 時,這將強制我們在構(gòu)造函數(shù)中設置所有屬性值。
正如所見,TypeScript 的嚴格變量是上述所有標志的簡寫。我們可以通過使用嚴格或逐步啟用它們來啟用它們。
更嚴格的類型將幫助我們在編譯時捕獲更多錯誤。
2、重新聲明接口
在鍵入組件接口時,通常需要具有相同類型的一些不同接口變體。這些可以在一兩個參數(shù)中變化。一個常見的錯誤是手動重新定義這些變體。這將導致:
不必要的樣板。
需要多次更改。如果一個屬性在一個地方發(fā)生變化,則需要將該更改傳播到多個文件。
很久以前,TypeScript 發(fā)布了一個旨在解決此目的的功能:映射類型。它們讓我們可以根據(jù)我們定義的一些規(guī)則,在現(xiàn)有類型的基礎上創(chuàng)建新類型。這確實會導致更具可讀性和聲明性的代碼庫。
讓我們看一個例子:
interface Book {author?: string;numPages: number;price: number;}// ? Article is a Book without a Pagetype Article = Omit<Book, 'numPages'>;// ? We might need a readonly verison of the Book Typetype ReadonlyBook = Readonly<Book>;// ? A Book that must have an authortype NonAnonymousBook = Omit<Book, 'author'> & Required<Pick<Book, 'author'>>;
在上面的代碼中,我們保留了一個單一的事實來源:Book 實體。它的所有變體都使用映射類型功能來表達,這大大減少了對代碼進行類型化和維護的成本。
映射類型也可以應用于聯(lián)合,如下所示:
type animals = 'bird' | 'cat' | 'crocodile';type mamals = Exclude<animals, 'crocodile'>;// 'bird' | 'cat'
TypeScript 附帶以下映射類型:Omit、Partial、Readonly、Exclude、Extract、NonNullable、ReturnType。
我們可以創(chuàng)建自己的實用程序并在我們的代碼庫中重用它們。
3、不依賴類型推斷
TypeScript 推理是這種編程語言最強大的工具之一。它為我們完成所有工作。我們只需要確保在盡可能少的干預下將所有部分加在一起。
實現(xiàn)這一目標的一個關鍵操作符是 typeof。它是一個類似于 JavaScript 的運算符。它不會返回 JavaScript 類型,而是返回 TypeScript 類型。使用這個操作數(shù)可以避免我們重新聲明相同的類型。
讓我們通過一個例子來看看:
const addNumber = (a: number, b: number) => a + b;// ? you are hardcoding the type `number` instead of relying on what the function returnsconst 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 itconst processResult = (result: ReturnType<typeof addNumber>) => console.log(result);processResult(addNumber(1, 1));
在上面的代碼中,注意結(jié)果參數(shù)類型。最好依賴 ReturnType<typeof addNumber> 而不是添加數(shù)字類型。通過對數(shù)字類型進行硬編碼,我們完成了編譯器的工作。
最好使用適當?shù)恼Z法來表達我們的類型。TypeScript 將為我們完成繁重的工作。
讓我們看一個虛擬示例:
// ? Sometimes for one of objects it is not necessary to define// interfaces for itinterface Book {name: string,author: string}const book: Book = {name: 'For whom the bell tolls',author: 'Hemingway'}const printBook = (bookInstance: Book) => console.log(bookInstance)
請注意,Book 接口用于特定場景,甚至不需要創(chuàng)建接口。
通過依賴 TypeScript 的推理,代碼變得不那么雜亂,更易于閱讀。下面是一個例子:
// ? For simple scenarios we can rely on type inferenceconst book = {name: 'For whom the bell tolls',author: 'Hemingway'}const printBook = (bookInstance: typeof book) => console.log(bookInstance)
TypeScript 甚至有 infer 運算符,它可以與 Mapped Types 結(jié)合使用以從另一個類型中提取一個類型。
const array: number[] = [1,2,3,4];// ? type X will be numbertype X = typeof array extends (infer U)[] ? U : never;
在上面的例子中,我們可以看到如何提取數(shù)組的類型。
4、不正確的使用 Overloading
TypeScript 本身支持重載。這很好,因為它可以提高我們的可讀性。但是,它不同于其他類型的重載語言。
在某些情況下,它可能會使我們的代碼更加復雜和冗長。為了防止這種情況發(fā)生,我們需要牢記兩條規(guī)則:
1. 避免編寫多個僅尾隨參數(shù)不同的重載
// ? instead of thisinterface Example {foo(one: number): number;foo(one: number, two: number): number;foo(one: number, two: number, three: number): number;}// ? do thisinterface Example {foo(one?: number, two?: number, three?: number): number;}
你可以看到兩個接口是如何相等的,但第一個比第二個更冗長。在這種情況下最好使用可選參數(shù)。
2. 避免僅在一種參數(shù)類型中編寫因類型不同而不同的重載
// ? instead of thisinterface Example {foo(one: number): number;foo(one: number | string): number;}// ? do thisinterface Example {foo(one: number | string): number;}
與前面的示例一樣,第一個界面變得非常冗長。最好使用聯(lián)合來代替。
5、使用函數(shù)類型
TypeScript 附帶 Function 類型。這就像使用 any 關鍵字但僅用于函數(shù)。遺憾的是,啟用嚴格模式不會阻止我們使用它。
這里有一點關于函數(shù)類型:
它接受任意數(shù)量和類型的參數(shù)。
返回類型始終為 any。
讓我們看一個例子:
// ? Avoid, parameters types and length are unknown. Return type is anyconst onSubmit = (callback: Function) => callback(1, 2, 3);// ? Preferred, the arguments and return type of callback is now clearconst onSubmit = (callback: () => Promise<unknown>) => callback();
在上面的代碼中,通過使用顯式函數(shù)定義,我們的回調(diào)函數(shù)更具可讀性和類型安全性。
6、依賴第三方實現(xiàn)不變性
在使用函數(shù)式編程范式時,TypeScript 可以提供很大幫助。它提供了所有必要的工具來確保我們不會改變我們的對象。我們不需要在我們的代碼庫中添加像 ImmutableJS這樣的笨重的庫。
讓我們通過以下示例來看看我們可以使用的一些工具:
// ? declare properties as readonlyinterface Person {readonly name: string;readonly age: number;}// ? implicitely declaring a readonly arraysconst x = [1,2,3,4,5] as const;// ? explicitely declaring a readonly arrayconst y: ReadonlyArray<{ x: number, y: number}> = [ {x: 1, y: 1}]interface Address {street: string;city: string;}// ? converting all the type properties to readonlytype ReadonlyAddress = Readonly<Address>;
正如你從上面的例子中看到的,我們有很多工具來保護我們的對象免于變異。
通過使用內(nèi)置功能,我們將保持我們的 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類型表示值是不會發(fā)生的類型。
我們來看一個例子:
interface HttpResponse<T, V> {data: T;included?: V;}type StringHttpResponse = HttpResponse<string, never>;// ? included prop is not assignableconst fails: StringHttpResponse = {data: 'test',included: {}// ^^^^^// Type '{}' is not assignable to type 'never'}// ? included is not assignedconst works: StringHttpResponse = {data: 'test',}
在上面的代碼中,我們可以使用 never 類型來表示我們不希望某個屬性是可賦值的。
我們可以將 Omit Mapped 類型用于相同的目的:
type StringHttpResponse = Omit<HttpResponse<string, unkown>, 'included'>;但是,你可以看到它的缺點。它更冗長。如果你檢查 Omit 的內(nèi)部結(jié)構(gòu),它會使用 Exclude,而后者又使用 never 類型。
通過依賴 infer 和 never 關鍵字,我們省去了復制任何類型的麻煩,并更好地表達我們的接口。
總結(jié)
這些指南易于遵循,旨在幫助你接受 TypeScript,而不是與之抗爭。TypeScript 旨在幫助你構(gòu)建更好的代碼庫,而不是妨礙你。
通過應用這些簡單的技巧,你將擁有一個更好、更簡潔且易于維護的代碼庫。
我們是否遺漏了你項目中經(jīng)常發(fā)生的任何常見錯誤?請在評論中與我分享它們。
感謝你的閱讀。
學習更多技能
請點擊下方公眾號
![]()

