在這篇文章中,我們將探討一段精妙的 TypeScript 程式碼,它展示了如何透過 泛型 來創建一個動態且型別安全的服務系統。
這個範例可以用來在應用中定義並處理一系列的服務方法,透過 TypeScript 的型別系統保證每個服務方法的參數和行為都符合預期。
TypeScript: TS Playground - An online editor for exploring TypeScript and JavaScript
ServiceDefinition
)export type ServiceDefinition = {
[x: string]: MethodDefinition;
};
這段程式碼定義了一個服務的基本結構,ServiceDefinition
是一個物件,物件中的每個鍵都是一個服務方法的名稱,對應的值是該方法的定義,這些定義會告訴我們這個方法接收什麼樣的參數。
MethodDefinition
)export type MethodDefinition = {
[x: string]: StringConstructor | NumberConstructor;
};
MethodDefinition
定義了每個方法可以接受的參數型別。這裡,我們允許每個方法的參數是 String
或 Number
,這代表服務方法的參數型別只會是字串或數字。
ServiceObject
)export type ServiceObject<T extends ServiceDefinition> = {
[P in keyof T]: ServiceMethod<T[P]>
};
這個型別用來定義我們的服務物件,ServiceObject
會根據 ServiceDefinition
來生成對應的方法。這樣每個定義的服務方法都會被轉換成 ServiceMethod
,並且具備動態的參數型別。
ServiceMethod
)export type ServiceMethod<T extends MethodDefinition> = {} extends T
? () => boolean
: (payload: RequestPayload<T>) => boolean;
ServiceMethod
是核心邏輯,它依據方法的定義來決定這個方法是否接收參數:
T
) 是空的,則這個方法不需要參數,會是一個單純的 () => boolean
方法。payload
,並返回 boolean
。payload
(RequestPayload
)export type RequestPayload<T extends MethodDefinition> = {} extends T
? undefined
: { [P in keyof T]: TypeFromConstructor<T[P]> };
RequestPayload
會根據 MethodDefinition
自動推斷這個方法需要的 payload
型別。如果方法定義是空的,那 payload
會是 undefined
;如果方法定義了參數,那麼 payload
會是對應的物件,物件中的每個屬性對應到方法中的參數。
TypeFromConstructor
)export type TypeFromConstructor<T> = T extends StringConstructor
? string
: T extends NumberConstructor ? number : any;
這裡是型別轉換邏輯,根據 Constructor
類型決定對應的實際型別:
StringConstructor
轉換為 string
。NumberConstructor
轉換為 number
。這樣的設計可以保證我們在呼叫服務方法時,所傳遞的參數符合正確的型別。
RequestHandler
和 RequestObject
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
(參數)。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
函式,然後根據服務定義動態生成對應的方法。
每個方法在被呼叫時,會將 message
和 payload
傳給 handler
,這樣可以對每個請求進行統一處理。
const serviceDefinition = {
open: { filename: String },
insert: { pos: Number, text: String },
delete: { pos: Number, len: Number },
close: {},
};
這裡定義了四個服務方法:
open
:需要一個 filename
參數(字串)。insert
:需要 pos
(數字)和 text
(字串)兩個參數。delete
:需要 pos
和 len
兩個數字參數。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
進行對應的操作處理。
service.close();
service.open({ filename: 'text.txt' });
最後,我們可以像使用一般物件方法一樣來呼叫服務方法。service.close()
不需要參數,而 service.open({ filename: 'text.txt' })
則需要提供一個 filename
參數。
型別定義錯誤:當 MethodDefinition
定義的參數不符合 StringConstructor
或 NumberConstructor
時,可能導致型別錯誤。因此,務必確保在 ServiceDefinition
中定義的方法參數型別正確。
處理函式錯誤:如果 handler
沒有正確處理 RequestObject
中的 message
和 payload
,可能會導致 undefined
或型別錯誤。
遞歸型別推斷問題:在某些情況下,遞歸型別推斷可能會遇到型別系統的限制,導致推斷失敗。這時可以考慮進一步簡化型別邏輯。
動態服務系統設計:透過 TypeScript 泛型與高階型別,實現靈活的服務方法定義,能動態處理多種不同的請求。
型別安全保障:使用 JSONified
和遞歸型別推斷,保證每個服務方法的參數和返回值符合預期,避免型別錯誤。
高度可擴展性:新增或修改服務方法時,只需更新 ServiceDefinition
,型別系統會自動推斷並生成對應的邏輯。
undefined
處理得當:使用 UndefinedAsNull
將 undefined
轉換為 null
,避免 JSON 格式不支援 undefined
的問題。
簡化程式碼邏輯:通過型別系統的靈活應用,減少手動定義類型的需求,提升程式碼可讀性與可維護性。
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 的型別魔法!
別害怕嘗試新技巧,每一次的挑戰都是進步的機會~
快把這些知識用在你的專案裡,讓程式碼更靈活、更安全!
加油,寫程式就是這麼好玩!💻✨