TypeScript魔法堂:函數類型聲明其實很複雜

前言

江湖有傳「動態類型一時爽,程式碼重構火葬場」,由於動態類型語言在開發時不受數據類型的約束,因此非常適合在項目原型階段和初期進行快速迭代開發使用,這意味著項目未來將通過重寫而非重構的方式進入成熟階段。而在企業級應用開發中,每個系統特性其實都是需求分析人員與用戶進行多次調研後明確下來的,後期需要重寫的可能性微乎其微,更多的是修修改改,在單元測試不足常態化的環境下靜態類型的優勢就尤為突出。而TypeScript的類型系統和編譯時類型檢查機制則非常適合用於構建企業級或不以重寫實現迭代升級的應用系通。
本系列將重點分享TypeScript類型聲明相關實踐

  1. 函數類型聲明其實很複雜
  2. 玩轉交叉類型和聯合類型
  3. class,inteface和type到底選哪個?
  4. 從lib.d.ts學習外部類型聲明的最佳實踐
  5. 類型聲明綜合實戰

本文為該系列的首發,那麼我們現在就開始吧!

定義即聲明

當我們通過TypeScript定義函數時,實際上已經聲明了函數簽名和定義了函數體。

function foo(message: string, count?: number, displayLog = true): never {
    console[displayByLog ? 'log' : 'warn'](`message: ${message}; count: ${count}`)
    throw new Error('Just a error.')
}

上述函數定義附帶聲明了function foo(x: boolean, y: string, z: undefined | number): never函數簽名,這裡我特意替換參數名稱以便大家將關注點放在函數參數列表類型和返回值類型上。
後續通過如下程式碼調用foo函數

foo('hi') // 回顯 message: hi; count: undefined
foo('hi', 'yes') // 編譯報錯

函數重載

JavaScript中我們會通過函數重載來整合處理入參數據結構存在差異,但處理意圖和處理結果相同的行為。具體實現方式有

function querySelector(x, parent) {
    var arg1 = typeof x === 'string' ? 0 : 1
    var arg2 = parent instanceof HTMLElement ? 0 : 1
    return (querySelector.overloads[arg1][arg2]).call(null, x, parent)
}
function q00 (x /*: string*/, p /*: HTMLElement*/) {
  return p.querySelector(x)
}
function q01 (x /*: string*/, p /*: JQuery*/) {
  return p.find(x)[0]
}
function q10 (x /*: JQuery*/, p /*: HTMLElement*/) {
  return $(p).find(x)[0]
}
function q11 (x /*: JQuery*/, p /*: JQuery*/) {
  return p.find(x)[0]
}

querySelector.overloads = [[q00,q01],[q10,q11]]

而TypeScript中的函數重載並沒有讓我們定義得更輕鬆,可以理解為在原JavaScript實現的基礎上添加類型聲明資訊,這樣反而讓定義變得複雜,但為了能更安全地調用卻是值得的。
寫法1:

function querySelector(x: string, p: HTMLElement): HTMLElement
function querySelector(x: string, p: JQuery): HTMLElement
function querySelector(x: JQuery, p: HTMLElement): HTMLElement
function querySelector(x: JQuery, p: JQuery): HTMLElement
// 和JavaScript一樣需要定義一個Dispatch函數,用於實現調用重載函數的具體規則
function querySelector(x, y) {
    var arg1 = typeof x === 'string' ? 0 : 1
    var arg2 = parent instanceof HTMLElement ? 0 : 1
    if (arg1 === 0 && arg2 === 0) {
        return p.querySelector(x)
    }
    else if (arg1 === 0 && arg2 === 1) {
        return p.find(x)[0]
    }
    else if (arg1 === 1 && arg2 === 0) {
        return $(p).find(x)[0]
    }
    else {
        return p.find(x)[0]
    }
}

寫法2:

interface QuerySelector{
    (x: string, p: HTMLElement): HTMLElement
    (x: string, p: number): HTMLElement
    (x: number, p: HTMLElement): HTMLElement
    (x: number, p: number): HTMLElement
    overloads: Function[][]
}
// 和JavaScript一樣需要定義一個Dispatch函數,用於實現調用重載函數的具體規則
let querySelector: QuerySelector= <QuerySelector>function (x: string | number, p: HTMLElement | number): HTMLElement { 
    let arg1 = typeof x === 'string' ? 0 : 1
    let arg2 = parent instanceof HTMLElement ? 0 : 1
    return (querySelector.overloads[arg1][arg2]).call(null, x, parent)
}
function q00 (x: string, p: HTMLElement):HTMLElement {
  return p.querySelector(x)
}
function q01 (x: string, p: JQuery):HTMLElement {
  return p.find(x)[0]
}
function q10 (x: JQuery, p: HTMLElement):HTMLElement {
  return p.find(x)[0]
}
function q11 (x: JQuery, p: JQuery):HTMLElement {
  return p.find(x)[0]
}
querySelector.overloads = [[q00, q01],[q10, q11]]

寫法2注意事項:

  1. Dispatch函數必須採用<T>作為類型斷言而不能使用as進行類型轉;
  2. Dispatch函數必須通過function方式定義,而不能使用箭頭函數方式定義。
    如果想以箭頭函數的方式定義Dispatch函數,那麼寫法就會更複雜了。
interface QuerySelector{
    (x: string, p: HTMLElement): HTMLElement
    (x: string, p: number): HTMLElement
    (x: number, p: HTMLElement): HTMLElement
    (x: number, p: number): HTMLElement
}
interface Overload {
    overloads: Function[][]
}
let querySelector: <QuerySelector & Overload>
let querySelectorDispatch:<QuerySelector> = (x: string | number, p: HTMLElement | number): HTMLElement => { 
    let arg1 = typeof x === 'string' ? 0 : 1
    let arg2 = parent instanceof HTMLElement ? 0 : 1
    return (querySelector.overloads[arg1][arg2]).call(null, x, parent)
}

function q00 (x: string, p: HTMLElement):HTMLElement {
  return p.querySelector(x)
}
function q01 (x: string, p: JQuery):HTMLElement {
  return p.find(x)[0]
}
function q10 (x: JQuery, p: HTMLElement):HTMLElement {
  return p.find(x)[0]
}
function q11 (x: JQuery, p: JQuery):HTMLElement {
  return p.find(x)[0]
}
querySelector = querySelectorDispatch as QuerySelector & Overload
querySelector.overloads = [[q00, q01],[q10, q11]]

累死人了。。。。。。。

高階函數的類型聲明

高階函數作為JavaScript最為人稱道的特性,在TypeScript中怎能缺席呢?

// 1
let foo1: (message: string, count?: number, displayLog?: boolean) => never

// 2
interface FooDecl {
  (message: string, count?: number, displayLog?: boolean): never
}
let foo2: FooDecl 

// 3
let foo3: {(message: string, count?: number, displayLog?: boolean): never}

// 4
type FooType = (message: string, count?: number, displayLog?: boolean) => never

上述為4種聲明高階函數類型的寫法,其中第3種是第2種的簡寫形式。
1、2和3方式聲明了變數的值類型,而2中的interface FooDecl和4中則聲明類型本身。
foo1,foo2,foo3作為變數(value)可作為傳遞給函數的實參,和函數的返回值。因此針對它們的值類型聲明是無法被重用的,也無法用於函數聲明和其它類型聲明中;
FooDecl,FooType作為類型聲明,及可以被反覆重用在各函數聲明和其它類型聲明中。

函數類型兼容

函數類型兼容的條件:

  1. 形參列表個數小於等於目標函數類型的形參列表個數;
  2. 形參列表中形參類型的順序和目標函數類型的形參列表一致,或形參類型為目標函數類型相應位置的參數類型的子類型;
  3. 函數返回值必須為目標函數類型返回值的子類型。
const add: (x: number, y: number) => number = (x, y) => x + y
const increment(x: number) => number = x => x+1

add = increment // 類型兼容
increment = add // 類型不兼容

const handleEvent: (e: Event) => void;
const handleMouseEvent: (e: MouseEvent) => void;
   
handleEvent = handleMouseEvent // 類型兼容
handleMouseEvent = handleEvent // 類型不兼容

總結

函數類型聲明難點在於函數重載這一塊,而作為庫開發者函數重載往往能幫助我們開發出更容易記憶使用和優雅的介面,既然逃不過那不如好好努力克服困難吧!

轉載請註明來自://www.cnblogs.com/fsjohnhuang/p/13903589.html —— ^_^肥仔John