🔧 阿川の電商水電行
Shopify 顧問、維護與客製化
💡
小任務 / 單次支援方案
單次處理 Shopify 修正/微調
⭐️
維護方案
每月 Shopify 技術支援 + 小修改 + 諮詢
🚀
專案建置
Shopify 功能導入、培訓 + 分階段交付

優雅永遠不過時!你試過這樣的函數重載嗎?

image

之前和大家提過,想要寫一個多參重載場景中能夠將函數實現分離的通用方法。經過一些天在類型體操中的折騰,今天,它來了!

如果你也遇到類似問題或場景,開發和維護陷入混亂和掙扎的話,不妨一起來看看。

前言

在編寫一些通用庫/通用方法時,為了支持多種使用方式,就需要有多個參數,參數的數量和類型不同,所對應的處理方法也不同。

假如現在有這樣的需求:

05.png

是不是已經開始頭皮發麻了呢,不過別慌,我們一步步來看。很明顯這是一個函數重載的場景,TS 原生支持函數重載,于是我們會這樣寫:

06.png

吼!這個參數的類型,要暈了,于是甚至可能這樣寫:

07.png

直接擺爛,參數交給 any[] ,變成 anyscript。幾乎放棄了函數實現裡的類型推導和檢查。這還只是函數簽名而已,函數體怎麼寫,可能會是這樣:

08.png

一眼望不到頭的 if else,這可怎麼辦?有一種經典的解決方案叫參數歸一化

09.png

但其實也只是把 if else 換了個位置,雖然相比之下簡潔方便了很多,但還是不夠優雅。

這個時候突然想要推出一個新功能,還要添加新的函數簽名,吼🙃!又要埋進寫好的代碼中摸索。😱

TS 函數重載不夠強大的根本原因在於它只存在於編譯時,要實現真正的重載肯定需要嵌入運行時代碼。如果能夠以極少量代碼實現運行時的函數匹配,並在開發過程中保留完整的類型檢查和提示,將函數實現真正分離,那開發體驗肯定會大大提高。

想一想,如果我們能這樣寫代碼:

10.png

整潔的觀感,一目了然的函數簽名定義,各個函數實現完全分離,不再是滿屏的 if else,並且保持完善的 TS 類型檢查和代碼提示。將來如果增加一些新的函數簽名,也不需要修改已有代碼,遵循開閉原則。

是不是感覺優雅了很多呢!話不多說,一起來看看吧。

聽了沒感覺?要試過才知道。在線演練場,來試一試吧

優雅永不過時!!!


安裝

npm install overload-func

使用

  1. 定義重載

調用 createOverloadedFunction 方法,需要一個類型參數,傳入一個數組,每一項都是一個函數類型。

import { createOverloadedFunction } from 'overload-func';

const func = createOverloadedFunction<[
  (a: string) => string,
  (a: number, b: number) => boolean
]>();
  1. 添加實現

調用 addImple 方法,最後一個參數為函數實現,之前的各個參數都是字符串,對應不同的參數類型。

func.addImple('string', (a) => {
  return a;
});
func.addImple('number', 'number', (a, b) => {
  return a > b;
});

TS 會根據傳入的參數類型,自動推導匹配對應的函數類型。

02.png

如果匹配不到相應的函數類型,或者定義的實現函數參數或返回值類型不匹配,TS 就會報錯,擁有完善的類型檢查和提示。

03.png

小技巧:調用 addImple 方法時,先寫好最後一個函數參數占位,再寫前面的參數類型,就可以隨時獲得代碼補全提示,

01.png

更多內置類型詳見 內置類型

  1. 調用

和 TS 原生的函數重載一樣,調用時只需要傳入正確的參數類型即可。

const r1 = func('hello'); // string
const r2 = func(1, 2); // boolean

會自動匹配到對應的函數實現,並返回結果,並且 TS 也會提示出正確的返回類型。

在線演練場,上手試一試吧

使用細節

內置類型

內置的類型支持:(字符串 -- 對應類型)

  • string -- string
  • number -- number
  • boolean -- boolean
  • null -- null
  • undefined -- undefined
  • symbol -- symbol
  • bigint -- bigint
  • function -- Function
  • array -- any[]
  • date -- Date
  • map -- Map
  • set -- Set
  • weakmap -- WeakMap
  • weakset -- WeakSet
  • regexp -- RegExp
  • promise -- Promise
  • error -- Error
  • object -- object

目前支持這些類型,包含所有基本類型,以及一些常用的內置類型。能夠滿足大部分的場景。

需要注意的是,object 類型不能和其他內置類型匹配,例如 any[]Map 等,這些類型本該滿足 extends object 的條件,但是為了更好的作區分,內部判斷時不為 object 類型的其他內置類型,是不會被認為匹配 object 類型的。例如這樣:

const fun = createOverloadedFunction<[
  (a: string[]) => string
]>();
fun.addImple('object', (a) => a.join('')); // error
fun.addImple('array', (a) => a.join(''));

11.png

你不能拿 object 參數去匹配 string[],雖然這在 TS 中看起來是正常的,但在這裡你需要用 array 來匹配數組類型。

源碼中使用了一個 LooseEqual 類型工具來匹配函數參數類型

export type LooseEqual<X, Y> = Equal<Y, object> extends true
  ? X extends BaseType
    ? false
    : X extends Y
      ? true
      : false
  : X extends Y ? true : false;

其中 BaseTypeobject 以外的其他內置類型object 類型會單獨處理,不會和其他內置類型匹配。

可選參數

目前不支持在函數簽名中使用可選參數

例如:(a: number, b?: string) => boolean,如果使用這樣的可選參數,使用中可能會出錯的。因為類似於 func(1) 這樣的調用,沒法正確匹配到函數實現。暫時還沒有想到好的解決方案。

我們可以通過下面的方式處理需要可選參數的場景。

const fn = createOverloadedFunction<[
  (a: number) => boolean,
  (a: number, b: string) => boolean,
]>();

不過話說回來,可選參數的場景,在函數實現中就存在判斷參數類型的邏輯。這好像和我們使用這個庫編寫重載代碼的初衷相悖吧😂。當然大家有什麼好的想法,歡迎交流指教。

結構化類型

TS 是結構化類型系統,所以我們在推導類型、定義使用重載、處理使用中遇到的問題時,一定要從結構化類型的角度出發來考慮問題。

看下面的例子(使用了後面會介紹到的 擴展類型

class Person {
  constructor(public name: string, public age: number) {}
}
const extendType = createExtendType({
  person: Person,
});
const fn = createOverloadedFunction<[
  (a: { name: string, age: number }) => number,
  (a: Person) => boolean
], typeof extendType>({
  extendType: extendType
});
fn.addImple('object', (a) => a.age);
fn.addImple('person', (a) => a.age > 18); // error

在上面的例子中,兩個實現匹配到的都是第一個函數簽名(運行起來雖然會得到想要的結果,但 TS 會報錯)。因為 TS 是結構化類型,Person 類型和 { name: string, age: number } 是兼容的。

如果確實需要上面的功能,就需要兩個對象擁有明確區別的屬性。我們可以為 Person 添加一個 gender 屬性,為 { name: string, age: number } 添加一個 id 屬性。這樣一來,就能正確匹配到各自的函數簽名。

image.png

高階指引

createOverloadedFunction 方法支持一些配置選項,可以更靈活地定制函數重載。

為一個重載添加多個實現

默認情況下,一個重載只允許添加一個實現。如果需要允許多個實現,可以設置 allowMultiple 配置選項,設置為 true 時,可以為一個重載添加多個實現。

const func = createOverloadedFunction<[
  (a: string) => string,
  (a: number, b: number) => boolean
]>({ allowMultiple: true });

func.addImple('string', (a) => {
  console.log('first implementation');
  return a;
});
func.addImple('string', (a) => {
  console.log('second implementation');
  return a.toUpperCase();
});

const r1 = func('hello'); // HELLO

此時,調用函數並傳入一個 string 類型參數,會依次調用兩個實現函數。但是要注意,返回值為最後一個實現函數的返回值

12.png

擴展類型

extendType 參數允許擴展類型支持,可以為 addImple 方法擴展可選類型參數。

通過創建類來定義類型,傳入對象,鍵名將作為 addImple 方法的可選類型參數,類作為鍵值。這裡推薦使用 createExtendType 方法創建擴展類型(可以得到更好的類型檢查)。

  1. 把函數的返回值傳入 extendType 參數
  2. 把函數返回值的類型傳入 createOverloadedFunction 的第二個類型參數
class Teacher {
  salary: number;
  constructor(public name: string) {}
}
class Student {
  score: number;
  constructor(public name: string) {}
}
const extendType = createExtendType({
  teacher: Teacher,
  student: Student,
});
const test = createOverloadedFunction<[
  (t: Teacher) => string,
  (s: Student) => number,
], typeof extendType>({
  extendType: extendType
});
test.addImple('teacher', (t) => t.name);
test.addImple('student', (s) => s.name.length);
const res1 = test(new Teacher('John'));
const res2 = test(new Student('Alice'));
console.log(res1, res2); // John 5

正如之前 結構化類型 中提到的問題,TS 是結構化類型系統。所以上面的例子中,為了區分 TeacherStudent,它們必須擁有能夠區分彼此的不同屬性。

當通過 extendType 擴展類型時,addImple 方法的可選類型參數就會增加 teacherstudent,同時也會有相應的代碼提示。

04.png

在類中使用

要在類中使用函數,重要的一點就是正確處理 this 的指向,並且在 TS 類型中正確推導它。如果你 TS 寫得還不錯,那這和上面例子的使用並沒有大的區別。

const test = createOverloadedFunction<[
  (this: Test, n: number) => boolean,
  (this: Test, n: string, s: string) => string,
]>();

test.addImple('number', function(n) {
  return n > this.count;
});
test.addImple('string', 'string', function(n, m) {
  return n + m;
});
class Test {
  count = 10;
  test = test;
}

const t = new Test();

console.log(t.test(8));
console.log(t.test('pknk', 'lll'));
  1. 在定義函數簽名時,需要使用 this 類型來指定 this 的指向。
  2. 在添加實現函數時,不能使用箭頭函數,而是使用普通函數。這是 JS 基礎知識,這裡就不做贅述。

這樣就可以實現重載的同時,擁有正確的 this 類型推導。


寫在最後

靈感源自於 渡一教育-袁老師 的一期短視頻(感興趣的朋友抖音搜索觀看),在其基礎上添加了完善的 TS 支持,並且改進了部分實現,拓展了更多功能。

其實這個庫打包後的運行時 JS 代碼一共也沒有多少,重點在於完善的 TS 支持,提升開發體驗和可維護性。

當然可能還有問題、bug、或是其他我沒有考慮到的地方,歡迎交流呀。👉🏻評論區 ✈️ issue

附上源碼地址: github 地址    gitee 地址

感覺怎麼樣,來試一試吧


原文出處:https://juejin.cn/post/7552066397812637750


精選技術文章翻譯,幫助開發者持續吸收新知。

共有 0 則留言


精選技術文章翻譯,幫助開發者持續吸收新知。
🏆 本月排行榜
🥇
站長阿川
📝10   💬10   ❤️3
446
🥈
我愛JS
📝4   💬9   ❤️10
230
🥉
AppleLily
📝1   💬4   ❤️1
63
#4
💬1  
5
#5
xxuan
💬1  
3
評分標準:發文×10 + 留言×3 + 獲讚×5 + 點讚×1 + 瀏覽數÷10
本數據每小時更新一次
🔧 阿川の電商水電行
Shopify 顧問、維護與客製化
💡
小任務 / 單次支援方案
單次處理 Shopify 修正/微調
⭐️
維護方案
每月 Shopify 技術支援 + 小修改 + 諮詢
🚀
專案建置
Shopify 功能導入、培訓 + 分階段交付