TypeScript魔法堂:函式型別宣告其實很複雜

^_^肥仔John發表於2020-11-02

前言

江湖有傳“動態型別一時爽,程式碼重構火葬場”,由於動態型別語言在開發時不受資料型別的約束,因此非常適合在專案原型階段和初期進行快速迭代開發使用,這意味著專案未來將通過重寫而非重構的方式進入成熟階段。而在企業級應用開發中,每個系統特性其實都是需求分析人員與使用者進行多次調研後明確下來的,後期需要重寫的可能性微乎其微,更多的是修修改改,在單元測試不足常態化的環境下靜態型別的優勢就尤為突出。而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 // 型別不相容

總結

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

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

相關文章