iT邦幫忙

2024 iThome 鐵人賽

DAY 16
0
JavaScript

我推的TypeScript 操作大全系列 第 16

我推Day16 - 深度解析 TypeScript 泛型與動態服務系統

  • 分享至 

  • xImage
  •  

https://ithelp.ithome.com.tw/upload/images/20240930/201244628SrhgbwW0F.jpg


深度解析:TypeScript 泛型與動態服務系統

在這篇文章中,我們將探討一段精妙的 TypeScript 程式碼,它展示了如何透過 泛型 來創建一個動態且型別安全的服務系統。
這個範例可以用來在應用中定義並處理一系列的服務方法,透過 TypeScript 的型別系統保證每個服務方法的參數和行為都符合預期。


演練區

TypeScript: TS Playground - An online editor for exploring TypeScript and JavaScript

1. 服務定義 (ServiceDefinition)

export type ServiceDefinition = {
  [x: string]: MethodDefinition;
};

這段程式碼定義了一個服務的基本結構,ServiceDefinition 是一個物件,物件中的每個鍵都是一個服務方法的名稱,對應的值是該方法的定義,這些定義會告訴我們這個方法接收什麼樣的參數。


2. 方法定義 (MethodDefinition)

export type MethodDefinition = {
  [x: string]: StringConstructor | NumberConstructor;
};

MethodDefinition 定義了每個方法可以接受的參數型別。這裡,我們允許每個方法的參數是 StringNumber,這代表服務方法的參數型別只會是字串或數字。


3. 服務物件 (ServiceObject)

export type ServiceObject<T extends ServiceDefinition> = {
  [P in keyof T]: ServiceMethod<T[P]>
};

這個型別用來定義我們的服務物件,ServiceObject 會根據 ServiceDefinition 來生成對應的方法。這樣每個定義的服務方法都會被轉換成 ServiceMethod,並且具備動態的參數型別。


4. 服務方法 (ServiceMethod)

export type ServiceMethod<T extends MethodDefinition> = {} extends T
  ? () => boolean
  : (payload: RequestPayload<T>) => boolean;

ServiceMethod 是核心邏輯,它依據方法的定義來決定這個方法是否接收參數:

  • 如果方法定義 (T) 是空的,則這個方法不需要參數,會是一個單純的 () => boolean 方法。
  • 如果方法有定義參數,則這個方法需要接收一個 payload,並返回 boolean

5. 請求的 payload (RequestPayload)

export type RequestPayload<T extends MethodDefinition> = {} extends T
  ? undefined
  : { [P in keyof T]: TypeFromConstructor<T[P]> };

RequestPayload 會根據 MethodDefinition 自動推斷這個方法需要的 payload 型別。如果方法定義是空的,那 payload 會是 undefined;如果方法定義了參數,那麼 payload 會是對應的物件,物件中的每個屬性對應到方法中的參數。


6. 轉換型別 (TypeFromConstructor)

export type TypeFromConstructor<T> = T extends StringConstructor
  ? string
  : T extends NumberConstructor ? number : any;

這裡是型別轉換邏輯,根據 Constructor 類型決定對應的實際型別:

  • StringConstructor 轉換為 string
  • NumberConstructor 轉換為 number

這樣的設計可以保證我們在呼叫服務方法時,所傳遞的參數符合正確的型別。


7. 處理請求的 RequestHandlerRequestObject

export type RequestHandler<T extends ServiceDefinition> = (req: RequestObject<T>) => boolean;

export type RequestObject<T extends ServiceDefinition> = {
  [P in keyof T]: {
    message: P;
    payload: RequestPayload<T[P]>;
  }
}[keyof T];
  • RequestHandler 定義了處理請求的邏輯,會接收一個 RequestObject 並返回 boolean
  • RequestObject 則是根據服務定義動態生成的物件,包含 message(方法名稱)和 payload(參數)。

8. 核心函式 createService

function createService<S extends ServiceDefinition>(
  serviceDef: S,
  handler: RequestHandler<S>,
): ServiceObject<S> {
  const service: Record<string, Function> = {};

  for (const name in serviceDef) {
    service[name] = (payload: any) => handler({ message: name, payload });
  }

  return service as ServiceObject<S>;
}

這個函式是核心功能,用於創建服務物件。它接收一個服務定義 serviceDef 和一個處理請求的 handler 函式,然後根據服務定義動態生成對應的方法。

每個方法在被呼叫時,會將 messagepayload 傳給 handler,這樣可以對每個請求進行統一處理。


9. 範例:定義與使用服務

const serviceDefinition = {
  open: { filename: String },
  insert: { pos: Number, text: String },
  delete: { pos: Number, len: Number },
  close: {},
};

這裡定義了四個服務方法:

  • open:需要一個 filename 參數(字串)。
  • insert:需要 pos(數字)和 text(字串)兩個參數。
  • delete:需要 poslen 兩個數字參數。
  • close:不需要參數。

接下來使用 createService 來創建服務:

const service = createService(serviceDefinition, req => {
  switch (req.message) {
    case 'open':
      break;
    case 'insert':
      req.payload;  // 可取得對應的 payload
      break;
    default:
      break;
  }

  return true;
});

這個 handler 函式會根據請求的 message 進行對應的操作處理。


10. 呼叫服務方法

service.close();
service.open({ filename: 'text.txt' });

最後,我們可以像使用一般物件方法一樣來呼叫服務方法。service.close() 不需要參數,而 service.open({ filename: 'text.txt' }) 則需要提供一個 filename 參數。


常見錯誤

  1. 型別定義錯誤:當 MethodDefinition 定義的參數不符合 StringConstructorNumberConstructor 時,可能導致型別錯誤。因此,務必確保在 ServiceDefinition 中定義的方法參數型別正確。

  2. 處理函式錯誤:如果 handler 沒有正確處理 RequestObject 中的 messagepayload,可能會導致 undefined 或型別錯誤。

  3. 遞歸型別推斷問題:在某些情況下,遞歸型別推斷可能會遇到型別系統的限制,導致推斷失敗。這時可以考慮進一步簡化型別邏輯。


總結小語:

  1. 動態服務系統設計:透過 TypeScript 泛型與高階型別,實現靈活的服務方法定義,能動態處理多種不同的請求。

  2. 型別安全保障:使用 JSONified 和遞歸型別推斷,保證每個服務方法的參數和返回值符合預期,避免型別錯誤。

  3. 高度可擴展性:新增或修改服務方法時,只需更新 ServiceDefinition,型別系統會自動推斷並生成對應的邏輯。

  4. undefined 處理得當:使用 UndefinedAsNullundefined 轉換為 null,避免 JSON 格式不支援 undefined 的問題。

  5. 簡化程式碼邏輯:通過型別系統的靈活應用,減少手動定義類型的需求,提升程式碼可讀性與可維護性。


  • 程式碼範例
export type ServiceDefinition = {
  [x: string]: MethodDefinition;
};

export type MethodDefinition = {
  [x: string]: StringConstructor | NumberConstructor;
};

export type ServiceObject<T extends ServiceDefinition> = {
  [P in keyof T]: ServiceMethod<T[P]>
};

export type ServiceMethod<T extends MethodDefinition> = {} extends T
  ? () => boolean
  : (payload: RequestPayload<T>) => boolean;

export type RequestPayload<T extends MethodDefinition> = {} extends T
  ? undefined
  : { [P in keyof T]: TypeFromConstructor<T[P]> };

export type TypeFromConstructor<T> = T extends StringConstructor
  ? string
  : T extends NumberConstructor ? number : any;

export type RequestHandler<T extends ServiceDefinition> = (req: RequestObject<T>) => boolean;

export type RequestObject<T extends ServiceDefinition> = {
  [P in keyof T]: {
    message: P;
    payload: RequestPayload<T[P]>;
  }
}[keyof T];


function createService<S extends ServiceDefinition>(
  serviceDef: S,
  handler: RequestHandler<S>,
): ServiceObject<S> {
  const service: Record<string, Function> = {};

  for (const name in serviceDef) {
    service[name] = (payload: any) => handler({ message: name, payload });
  }

  return service as ServiceObject<S>;
}

const serviceDefinition = {
  open: { filename: String },
  insert: { pos: Number, text: String },
  delete: { pos: Number, len: Number },
  close: {},
};

const service = createService(serviceDefinition, req => {
  switch (req.message) {
    case 'open':
      break;
    case 'insert':
      req.payload
      break;
    default:
     // req.
        break;
  }

  return true;
});


service.close();
service.open({ filename: 'text.txt' });


嘿嘿~看到這裡你已經掌握了 TypeScript 的型別魔法!
別害怕嘗試新技巧,每一次的挑戰都是進步的機會~

快把這些知識用在你的專案裡,讓程式碼更靈活、更安全!
加油,寫程式就是這麼好玩!💻✨


上一篇
我推Day15 - 技術大神專屬!TypeScript JSON 處理術你一定要學!
下一篇
我推Day17 - 型別安全大升級!用 TypeScript 防禦你的程式世界
系列文
我推的TypeScript 操作大全30
圖片
  直播研討會
圖片
{{ item.channelVendor }} {{ item.webinarstarted }} |
{{ formatDate(item.duration) }}
直播中

尚未有邦友留言

立即登入留言