使用 Typescript 的一些注意事項


背景

ts 用了一年了,回顧起來,也沒有那么順利。趁這兩天春節假期有時間,整理了幾個自己覺得需要注意的情況,復盤一下。

我上學時學過 java 和 C# ,畢業后又做了兩年 C# 全棧開發,對於靜態類型語言是有一定經驗的。ts 之所以能夠慢慢取代 js ,也是因為它是靜態類型語言。

但 ts 和 java 是不一樣的,本質是因為它作為一個靜態類型語言,要編譯成弱類型語言 js 來執行。所以,ts 只管得了編譯時,卻管不了運行時。下文的很多內容,都是這個特點的具體表現。

【個人提醒】我感覺 ts 為了能讓自己更適應 js 的轉型,做了很多非常繁瑣(或者叫靈活)的設計,我沒有詳細總結,但這種感覺很強烈。所以,如果你覺得 ts 有些地方過於繁瑣時,也不要擔心,這可能不是你的問題,而是它的問題。

任何美好的東西,都是應該簡單的、明確的。

易混亂的類型

如果問“ts 的變量有多少種類型”,你能否回答全面?ts 比 js 類型多一些。

never vs void

只需要記住一個特點:返回 never 的函數,都必須存在無法到達的終點,如死循環、拋出異常。

function fn1(): never {
	while(true) { /*...*/ }
}

function fn2(): never {
	throw new Error( /*...*/ )
}

any vs unknown

  • any 任何類型,會忽略語法檢查
  • unknown 不可預知的類型,不會忽略語法檢查(這就是最大區別)
const bar: any = 10;
any.substr(1); // OK - any 會忽略所有類型檢查

const foo: unknown = 'string';
foo.substr(1); // Error: 語法檢查不通過報錯
// (foo as string).substr(1) // OK
// if (typeof foo === 'string') { foo.substr(1) } // OK

一些“欺騙”編譯器語法檢查的行為

就如同你告訴編譯器:“按我寫的來,不要管太多,出了事兒我負責!”

編譯器不給你添麻煩了,不進行語法檢查了,但你一定要考慮好后果。所以,以下內容請慎用,不要無腦使用。

@ts-ignore

增加 @ts-ignore 的注釋,會忽略下一行的語法檢查。

const num1: number = 100
num1.substr() // Error 語法檢查錯誤

const num2: number = 200
// @ts-ignore
num2.substr() // Ok 語法檢查通過

any

如果 ts 是西游記,any 就是孫悟空,自由、無約束。了解西游記大部分是從孫悟空開始,了解 ts 可能也是從 any 開始用。

但西游記最后,孫悟空變成了佛。你的 any 也應該變成 interface 或者 type 。

類型斷言 as

文章一開始說過,ts 只管編譯時,不管運行時。as 就是典型的例子,你用 as 告訴編譯器類型,編譯器就聽你的。但運行時,后果自負。

function fn(a: string | null): void {
    const length = (a as string).length
    console.log(length)
}
fn('abc') // Ok
// fn(null) // Error js 運行報錯

非空斷言操作符 !

! 用於排除 null undefined ,即告訴編譯器:xx 變量肯定不是 nullundefined ,你放心吧~

同理,運行時有可能出錯。

// 例子 1
function fn(a: string | null | undefined) {
    let s: string = ''
    s = a // Error 語法檢查失敗
    s = a! // OK —— 【注意】如果 a 真的是 null 或者 undefined ,那么 s 也會是 null 或者 undefined ,可能會帶來 bug !!!
}
// fn(null)
// 例子 2
type NumGenerator = () => number;

function myFunc(numGenerator: NumGenerator | undefined) {
  const num1 = numGenerator(); // Error 語法檢查失敗
  const num2 = numGenerator!(); // OK
}

// myFunc(undefined) // 【注意】,如果真的傳入 undefined ,也會去執行,當然會執行報錯!!!
// 例子 3
let a: number
console.log(a) // Error - Variable 'n' is used before being assigned.
let b!: number
console.log(b) // OK - `!` 表示,你會給 b 一個賦值,不用編譯器關心

可選鏈 ?.

?. 遇到 nullundefined 就可以立即停止某些表達式的運行,並返回 undefined
【注意】這里只針對 nullundefined ,對於 0 false '' 等 falsely 變量是不起作用的。這一點和 && 不一樣。

這個運算符,看似是獲取一個屬性,其實它是有條件判斷的。即,它就是一個 ? : 三元表達式的語法糖。既然它有判斷邏輯,那你考慮不到位,就有可能出錯。

// 例子 1 - 獲取對象屬性
interface IFoo { a: number }

function fn(obj: IFoo | null | undefined): number | undefined {
    const a = obj?.a // ?. 可選鏈運算符

    // 第一,如果 a 是 IFoo 類型,則打印 100
    // 第二,如果 a 是 null 或者 undefined ,則打印 undefined
    console.log('a', a)

    return a // 100 或者 undefined
}
fn({ a: 100 })
// fn(null)
// fn(undefined)
// 例子 2 - 獲取數組元素
function tryGetArrayElement<T>(arr?: T[], index: number = 0) {
  return arr?.[index];
}
// 編譯產出:
// "use strict";
// function tryGetArrayElement(arr, index = 0) {
//     return arr === null || arr === void 0 ? void 0 : arr[index];
// }
// 例子 3 - 用於函數調用
type NumGenerator = () => number;
function fn(numGenerator: NumGenerator | undefined | null) {
  const num = numGenerator?.();
  console.log('num', num) // 如果不是函數,則不調用,也不會報錯,返回 undefined
}
// fn(null)
// fn(undefined)

【吐槽】對於這種語法糖,我還是比較反感的,我覺得自己寫幾行邏輯判斷會更好。它雖然簡潔,但是它會帶來閱讀理解上的負擔,代碼簡潔不一定就可讀性好 —— 當然了,如果大家都這么用,用久了,大家都熟悉了,可能也就沒有這個障礙了。

type 和 interface

我目前還是處於一種懵逼狀態。我感覺 type 和 insterface 有太多的灰色地帶,這就導致我們日常使用時,大部分情況下用誰都可以。我搞不懂 ts 為何要這樣設計。

按照我前些年對 java 和 C# 的理解:(我不知道近幾年 java C# 有沒有相關的語法變化)

  • 如果自定義一個靜態的類型,僅有一些屬性,沒有方法,就用 type
  • 如果定義一種行為(行為肯定是需要方法的,僅屬性是不夠的),需要 class 實現,就用 interface

但是查到的資料,以及查閱 ts 的類庫 lib.dom.d.ts 和 lib.es2015.d.ts 源碼,也都是用 interface 。我曾經一度很困惑,見的多了,就慢慢習慣成自然了,但問題並沒有解決。

問題沒有解決,但事情還是要繼續做的,代碼也是要繼續寫的,所以我就一直跟隨大眾,盡量用 interface 。

private#

兩者都表示私有屬性。背景不同:

  • private 是 ts 中一開始就有的語法,而且目前只有 ts 有,ES 規范沒有。
  • # 是 ES 目前的提案語法,然后被 ts 3.8 支持了。即,ts 和 ES 都支持 #

如果僅對於 ts 來說,用哪個都一樣。
但本文一開始提到過:ts 只關注編譯時,不關注運行時。所以,還得看看兩者的編譯結果。

private

private 編譯之后,就失去了私有的特點。即,如果你執行 (new Person()).name ,雖然語法檢查不通過,但運行時是可以成功的。
即,private 僅僅是 ts 的語法,編譯成 js 之后,就失效了。

// ts 源碼
class Person {
    private name: string
    constructor() {
        this.name = 'zhangsan'
    }
}

/* 編譯結果如下
"use strict";
class Person {
    constructor() {
        this.name = 'zhangsan';
    }
}
*/

#

# 編譯之后,依然具有私有特點,而且用 (new Person()).name ,在運行時也是無法實現的。
即,# 是 ts 語法,但同時也是 ES 的提案語法,編譯之后也不能失效。

但是,編譯結果中,“私有”是通過 WeekMap 來實現的,所以要確保你的運行時環境支持 ES6WeekMap 沒有完美的 Polyfill 方案,強行 Polyfill 可能會發生內存泄漏。

// ts 源碼
class Person {
    #name: string
    constructor() {
        this.#name = 'zhangsan'
    }
}

/* 編譯結果如下
"use strict";
var __classPrivateFieldSet = (this && this.__classPrivateFieldSet) || function (receiver, privateMap, value) {
    if (!privateMap.has(receiver)) {
        throw new TypeError("attempted to set private field on non-instance");
    }
    privateMap.set(receiver, value);
    return value;
};
var _name;
class Person {
    constructor() {
        _name.set(this, void 0);
        __classPrivateFieldSet(this, _name, 'zhangsan');
    }
}
_name = new WeakMap();
*/

函數重載

java 中的函數重載

java 中的函數重載是非常好用,而且非常好理解的,傻瓜式的,一看就懂。
如下代碼,定義了四個名為 test 的函數,參數不同。那就直接寫四個函數即可,調用時也直接調用,java 會自動匹配。

public class Overloading {
    public int test(){
        System.out.println("test1");
        return 1;
    }
    public void test(int a){
        System.out.println("test2");
    }   
    public String test(int a,String s){
        System.out.println("test3");
        return "returntest3";
    }   
    public String test(String s,int a){
        System.out.println("test4");
        return "returntest4";
    }   
    public static void main(String[] args){
        Overloading o = new Overloading();
        System.out.println(o.test());
        o.test(1);
        System.out.println(o.test(1,"test3"));
        System.out.println(o.test("test4",1));
    }
}

ts 中的函數重載

ts 的函數重載,先把各個情況的函數頭寫出來,然后再寫一個統一的、兼容上述所有情況的函數頭。最后,函數體自行處理參數。

class Person {
    // 第一,各個情況的函數頭寫出來
    test(): void
    test(a: number, b: number): number
    test(a: string, b: string): string
    // 第二,統一的、兼容上述所有情況的函數頭(有一個不兼容,就報錯)
    test(a?: string | number, b?: string | number): void | string | number {
        // 第三,函數體自行處理參數

        if (typeof a === 'string' && typeof b === 'string') {
            return 'string params'
        }
        if (typeof a === 'number' && typeof b === 'number') {
            return 'number params'
        }
        console.log('no params')
    }
}

這和 java 的語法比起來,簡直就是復雜 + 丑陋,完全違背設計原則
但是,為何要這樣呢?最終還是因為 ts 只關注編譯時,管不了運行時 —— 這是原罪。
試想,如果 ts 也設計像 java 一樣的重載寫法,那編譯出來的 js 代碼就會亂套的。因為 js 是弱類型的。

注意函數定義的順序

參數越精准的,放在前面。

/* 錯誤:any 類型不精准,應該放在最后 */
declare function fn(x: any): any;
declare function fn(x: HTMLElement): number;
declare function fn(x: HTMLDivElement): string;

var myElem: HTMLDivElement;
var x = fn(myElem); // x: any, wat?

不要為僅在末尾參數不同時寫不同的重載,應該盡可能使用可選參數。

/* 錯誤 */
interface Example1 {
    diff(one: string): number;
    diff(one: string, two: string): number;
    diff(one: string, two: string, three: boolean): number;
}

/* OK */
interface Example2 {
    diff(one: string, two?: string, three?: boolean): number;
}

DOM 相關的類型

Vue 和 React 框架的普及,讓大部分業務開發者不用直接操作 DOM ,變成了框架工程師。但 Web 是基於 DOM 的,可以不用,但千萬不要忘記。

js 寫 DOM 操作非常簡單,不用關心類型,直接訪問屬性和方法即可。但用 ts 之后,就得關心 DOM 操作的相關類型。

不光我們使用 ts ,微軟在設計 ts 時,也需要定義 DOM 操作相關的類型,放在 ts 的類庫中,這樣 ts 才能被 web 場景所使用。這些都定義在 lib.dom.d.ts 中。補:還有 ES 語法的內置類庫,也在同目錄下。

PS:一門成熟可用的編程語言,最基本的要包括:語法 + 類庫 + 編譯器 + 運行時(或者編譯器和運行時統一為解釋器)。然后再說框架,工具,包管理器等這些外圍配置。

Node Element 等類型

這些都是現成的,W3C 早就定義好了的,我們直接回顧一下就可以。我覺得一張圖就可以很好的表達,詳細的可以參考各自的 MDN 文檔。

事件參數類型

在使用 ts 之前,我並沒有特別關注事件參數類型(或者之前看過,后來不用,慢慢忘了),反正直接獲取屬性,拿來用就可以。

document.body.addEventListener('click', e1 => {
    // e1 的構造函數是什么?
})
document.body.addEventListener('keyup', e2 => {
    // e2 的構造函數是什么?
})

於是我查了一下 MDN 的文檔,其實也很好理解,就是不同的事件,參數類型是不一樣的,當然屬性、方法也就不一樣。下面列出我們常見的,所有的類型參考 MDN 這個文檔

事件 參數類型
click dbclick mouseup mousedown mousemove mouseenter mouseleave MouseEvent
keyup keyrpess keydown KeyboardEvent
compositionstart compositionupdate compositionend(輸入法) CompositionEvent
focus blur focusin focusout FocusEvent
drag drop DragEvent
paste cut copy ClipboardEvent

他們的繼承關系如下圖。其中 UIEvent 表示的是用戶在 UI 觸發的一些事件。因為事件不僅僅是用戶觸發的,還有 API 腳本觸發的,所以要單獨拿出一個 UIEvent ,作為區分。

總結

我感覺重點的就是那句話:ts 是一門靜態類型語言,但它要編譯成為 js 這個弱類型語言來執行,所以它管得了編譯時,卻管不了運行時。這是很多問題的根本。

目前看來,前端社區會慢慢往 ts 轉型,所以能熟練使用 ts 已經是一名前端人員必備的技能。希望本文能給大家帶來一點點幫助。

往期回顧:


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM