Mini-Project: Typed Config Loader
ဒီ Mini-Project အကြောင်း
Section titled “ဒီ Mini-Project အကြောင်း”ရိုးရှင်းပြီး Type အတိအကျ မှန်ကန်တဲ့ Configuration Loader တစ်ခုကို အတူတူ တည်ဆောက်ကြရအောင်။ ဒီစနစ်က အောက်ပါ တာဝန်တွေကို လုပ်ဆောင်ပေးပါမယ် -
- ကျွန်တော်တို့ရဲ့ App အတွက် လိုအပ်တဲ့ Setting တွေဟာ ဘယ်လို ပုံစံမျိုး (Type) ဖြစ်ရမယ် ဆိုတာကို အရင်ဆုံး သတ်မှတ်ပါမယ်။
- အပြင်ကနေ (ဥပမာ - JSON File ဒါမှမဟုတ် Environment Variables) ဝင်လာမယ့် Setting Data တွေကို ‘အတု’ ဖန်တီးပြီး ထည့်သွင်းကြည့်ပါမယ်။
- တစ်ခါတလေ Setting တွေက အပြည့်အစုံ ပါမလာတတ်ပါဘူး။ အဲ့ဒီအခါမျိုးမှာ လိုအပ်နေတဲ့ Setting တွေအတွက် ကြိုတင် သတ်မှတ်ထားတဲ့ ‘Default Values’ (ပုံမှန်တန်ဖိုး) တွေနဲ့ အလိုအလျောက် အစားထိုး ဖြည့်စွက်ပေးပါမယ်။
- နောက်ဆုံး ထွက်လာမယ့် ပြည့်စုံတဲ့ Setting Object ကြီးဟာ Type-safe (Type အတိအကျ မှန်ကန်ကြောင်း အာမခံချက်ရှိ) ဖြစ်ရမယ့်အပြင်၊ ပရိုဂရမ် အလုပ်လုပ်နေစဉ်မှာ ဘယ်သူမှ ထပ်မံ ပြင်ဆင်လို့ မရအောင် (Immutable/Readonly) သော့ခတ်ထားပေးပါမယ်။
အဆင့် ၁: Config ရဲ့ ပုံစံနဲ့ ‘Default Values’ တွေ သတ်မှတ်ခြင်း
Section titled “အဆင့် ၁: Config ရဲ့ ပုံစံနဲ့ ‘Default Values’ တွေ သတ်မှတ်ခြင်း”ပထမဆုံးအနေနဲ့ ကျွန်တော်တို့ App မှာ ဘာ Setting တွေ ပါရမလဲဆိုတာကို AppConfig ဆိုတဲ့ Interface လေးနဲ့ တိတိကျကျ သတ်မှတ်လိုက်ပါမယ်။
interface AppConfig { env: "development" | "production"; // ဒီ App ကို ဘယ်ပတ်ဝန်းကျင်မှာ Run မလဲ (development/production)။ port: number; // ဒီ App အလုပ်လုပ်မယ့် Port နံပါတ်။ (ဥပမာ - 3000) apiBaseUrl: string; // API တွေကို ဆက်သွယ်ဖို့ အဓိက URL လိပ်စာ။ featureFlags?: Record<string, boolean>; // App ရဲ့ အချို့သော Feature တွေကို အဖွင့်/အပိတ် လုပ်ဖို့ ထိန်းချုပ်မယ့် Flags တွေ။ (? ပါတဲ့အတွက် မဖြစ်မနေ ထည့်စရာမလိုပါဘူး)}အပြင်ကနေ Setting တွေ ပါမလာခဲ့ရင် အစားထိုး အသုံးပြုဖို့အတွက် ‘Default Values’ တွေကိုလည်း ကြိုတင် သတ်မှတ်ပါမယ်။ ဒီနေရာမှာ defaultConfig ရဲ့ Type ကို Partial လို့ သတ်မှတ်ထားတာကို သတိပြုပါ။
// ဒါတွေကတော့ Configuration တွေအတွက် ကြိုတင် သတ်မှတ်ပေးထားမယ့် Default Values တွေပါ။// Partial<AppConfig> လို့ သတ်မှတ်ထားတဲ့အတွက်၊ AppConfig ထဲက Properties အကုန်လုံး မထည့်ဘဲ// ကျွန်တော်တို့ Default အနေနဲ့ ထားချင်တဲ့ တချို့ Properties တွေကိုပဲ ရွေးချယ် ထည့်သွင်းလို့ ရပါတယ်။const defaultConfig: Partial<AppConfig> = { env: "development", // အပြင်ကနေ 'env' မပေးလိုက်ရင် "development" ကို အလိုအလျောက် သုံးမယ်။ port: 3000, // 'port' မပါရင် 3000 ကို သုံးမယ်။ featureFlags: { logging: true // 'logging' feature ကို ပုံမှန်အားဖြင့် ဖွင့်ထား (true) မယ်။ }};အဆင့် ၂: Config တွေကို Load လုပ်ပေးမယ့် Function တည်ဆောက်ခြင်း
Section titled “အဆင့် ၂: Config တွေကို Load လုပ်ပေးမယ့် Function တည်ဆောက်ခြင်း”အခုဆိုရင် ကျွန်တော်တို့ရဲ့ အဓိက အလုပ်လုပ်ပေးမယ့် ‘Config Loader’ Function လေးကို စတင် တည်ဆောက်ပါမယ်။ ဒီ Function ထဲမှာ Generics နဲ့ Utility Types တွေကို ဘယ်လို လှလှပပ အသုံးချထားသလဲ ဆိုတာကို လေ့လာကြည့်ပါ။
// T ဆိုတာက ကျွန်တော်တို့ မျှော်လင့်ထားတဲ့ Config ရဲ့ အပြည့်အစုံ Type (ဥပမာ AppConfig) ပါ။// D ဆိုတာက Default Values တွေရဲ့ Type (ဥပမာ Partial<AppConfig>) ပါ။function loadConfig<T extends object, D extends Partial<T>>( loadedRawConfig: Partial<T>, // အပြင်ကနေ (ဥပမာ - File/Database ထဲကနေ) ဖတ်လာမယ့် Config တွေ။ (အပြည့်အစုံ ပါချင်မှ ပါမှာမို့ Partial ပါ) defaultValues: D // ကျွန်တော်တို့ ကြိုတင် သတ်မှတ်ထားတဲ့ Default Values တွေ။): Readonly<T> { // ပြန်ထွက်လာမယ့် Result ဟာ 'T' ရဲ့ အပြည့်အစုံ ပုံစံဖြစ်ရမယ်၊ ပြီးတော့ ပြင်လို့မရအောင် Readonly ဖြစ်ရမယ်။
// ဒီ Function ရဲ့ အဓိက တာဝန်က Default Values တွေပေါ်မှာ၊ အပြင်က ဖတ်လာတဲ့ Setting တွေကို ထပ်ပိုးပြီး ပေါင်းစပ် (Merge) ဖို့ပါပဲ။ // အပြင်က ဖတ်လာတဲ့ Setting တွေမှာ Default နဲ့ နာမည်တူတာ ပါလာရင်၊ အပြင်ကဟာကို ဦးစားပေးပြီး အစားထိုးလိုက်ပါတယ်။ // ဒါကို 'Shallow Merge' (အပေါ်ယံ ပေါင်းစပ်ခြင်း) လို့ ခေါ်ပါတယ်။ const mergedConfig = { ...defaultValues, // အရင်ဆုံး Default Values တွေကို ချထားလိုက်တယ်။ ...loadedRawConfig, // ပြီးမှ အပြင်ကနေ ရလာတဲ့ Setting တွေနဲ့ အပေါ်ကနေ ထပ်ပြီး (Override လုပ်) ဖုံးလိုက်တယ်။ } as T; // 'as T' လို့ သုံးရတဲ့ အကြောင်းရင်းကတော့၊ ဒီပေါင်းစပ်ထားတဲ့ Object ကြီးဟာ နောက်ဆုံးမှာ T (ဥပမာ AppConfig) ရဲ့ စည်းမျဉ်းတွေနဲ့ ပြည့်စုံသွားလိမ့်မယ်လို့ TypeScript ကို အတည်ပြု ပြောပြလိုက်တာပါ။
// Object.freeze() ကို သုံးပြီး mergedConfig object ကြီးကို အသေ (Immutable) ဖြစ်အောင် လုပ်လိုက်ပါတယ်။ // ဒါဆိုရင် ဒီ Object ရဲ့ Properties တွေကို ပရိုဂရမ် အလုပ်လုပ်နေစဉ်မှာ (Runtime မှာ) ဘယ်သူမှ ထပ်ပြင်လို့ ရတော့မှာ မဟုတ်ပါဘူး။ return Object.freeze(mergedConfig);}loadConfig Function ရှင်းပြချက်:
Section titled “loadConfig Function ရှင်းပြချက်:”- Generics (T, D):
Tဆိုတာ ကျွန်တော်တို့ မျှော်လင့်ထားတဲ့ Config ရဲ့ အပြည့်အစုံ ပုံစံ (Type) ပါ (ဥပမာ -AppConfig)။Dဆိုတာကတော့ Default Values တွေရဲ့ Type ပါ။D extends Partialလို့ ရေးထားတဲ့အတွက်DဟာTရဲ့ အစိတ်အပိုင်း တစ်ခု (Partial) ဖြစ်ရမယ်လို့ ကန့်သတ်ထားပါတယ်။
- Input Parameters:
loadedRawConfig: Partialအပြင်ကနေ ရလာမယ့် Setting Data တွေပါ။ သူတို့က အမြဲတမ်း ပြည့်စုံနေမှာ မဟုတ်တဲ့အတွက်Partialလို့ သတ်မှတ်ထားပါတယ်။defaultValues: D:ကျွန်တော်တို့ ကြိုတင် ပြင်ဆင်ထားတဲ့ ပုံမှန်တန်ဖိုး (Default Values) တွေပါ။
- Return Type (
Readonly): ဒီ Function အလုပ်လုပ်ပြီးလို့ ပြန်ထွက်လာမယ့် (Return) နောက်ဆုံး Result ကြီးဟာT(AppConfig) ရဲ့ အပြည့်အစုံ ပုံစံ ဖြစ်ရပါမယ်။ ထပ်ပြီးတော့Readonlyဆိုတဲ့ Utility Type ကို လွှမ်းခြုံထားတဲ့အတွက် ဒီ Object ကြီးကို ဘယ်သူမှ ထပ်ပြင်လို့ ရတော့မှာ မဟုတ်ပါဘူး။ Object.freeze(mergedConfig): ဒါကတော့ TypeScript ရဲ့ Type အဆင့်မှာတင် မဟုတ်ဘဲ, JavaScript ရဲ့ Runtime အဆင့်မှာပါmergedConfigObject ကြီးကို တကယ် ပြင်လို့မရအောင် (Immutable ဖြစ်အောင်) သော့ခတ် (Freeze) ပစ်လိုက်တာ ဖြစ်ပါတယ်။
အဆင့် ၃: Config Loader ကို အသုံးပြုခြင်းနဲ့ satisfies ဖြင့် စစ်ဆေးခြင်း
Section titled “အဆင့် ၃: Config Loader ကို အသုံးပြုခြင်းနဲ့ satisfies ဖြင့် စစ်ဆေးခြင်း”ကဲ… အခု ကျွန်တော်တို့ တည်ဆောက်ထားတဲ့ loadConfig Function လေးကို လက်တွေ့ အသုံးပြု စမ်းသပ်ကြည့်ကြရအောင်။
ပထမဆုံး၊ အပြင်ကနေ ဝင်လာမယ့် Config Data လေးတစ်ခုကို အတုအယောင် ဖန်တီးပါမယ်။ ဒီနေရာမှာ satisfies Operator ကို သုံးပြီး Data ရဲ့ ပုံစံ မှန်ကန်မှုကို ဘယ်လို ထိန်းကျောင်းသွားသလဲ ဆိုတာကို သတိပြုကြည့်ပါ။
// ဒါက အပြင်ကနေ (ဥပမာ JSON File ထဲကနေ) ဖတ်လာတဲ့ Configuration Data လို့ မှတ်ယူကြည့်ပါ။// satisfies Operator ကို အသုံးပြုပြီး၊ ဒီ Data ဟာ Partial<AppConfig> နဲ့ ကိုက်ညီမှုရှိအောင် စစ်ဆေးထားပါတယ်။const configFileContent = { // env: "production", // 'env' ကို မပေးထားဘူးလို့ သဘောထားကြည့်ပါ။ Default (development) ကို ယူသွားပါလိမ့်မယ်။ port: 8080, apiBaseUrl: "/api/v2", featureFlags: { betaFeature: true, // logging をလည်း မပေးထားတဲ့အတွက် Default (true) ကို ယူသွားပါမယ်။ }, customServiceUrl: "http://my.service" // ဒီ Property ဟာ AppConfig ထဲမှာ မပါပါဘူး။ (အောက်မှာ ရှင်းပြထားပါတယ်)} satisfies Partial<AppConfig> & { customServiceUrl?: string };// 👆 'satisfies' က Partial<AppConfig> စည်းမျဉ်းနဲ့ ကိုက်ညီလား စစ်ဆေးပေးပြီး၊ တစ်ချိန်တည်းမှာပဲ 'customServiceUrl' လိုမျိုး အပို Properties တွေကိုပါ ထည့်သွင်းခွင့်ပြုထားတာပါ။
// တကယ်လို့သာ 'AppConfig' ထဲမှာ ပါတဲ့ အရာတွေကိုပဲ တိတိကျကျ စစ်ထုတ်ချင်ရင်တော့ အောက်ပါအတိုင်း Type Annotation (:) ကို သုံးနိုင်ပါတယ်။const filteredConfigFileContent: Partial<AppConfig> = { port: 8080, apiBaseUrl: "/api/v2", featureFlags: { betaFeature: true }};
// loadConfig Function ကို အသုံးပြုပြီး Configuration Data တွေနဲ့ Default Values တွေကို ပေါင်းစပ်လိုက်ပါမယ်。const finalAppConfig = loadConfig<AppConfig, Partial<AppConfig>>( filteredConfigFileContent, // ဒါမှမဟုတ် configFileContent ကို တိုက်ရိုက် ထည့်ပေးလို့လည်း ရပါတယ်။ defaultConfig);
console.log("ပေါင်းစပ်ပြီး ရလာတဲ့ နောက်ဆုံး Config:");console.log(finalAppConfig);
// Result တွေကို လေ့လာကြည့်ရအောင်:// finalAppConfig.env ဟာ "development" ဖြစ်နေပါမယ်။ (defaultConfig ကနေ ရလာတာပါ)// finalAppConfig.port ဟာ 8080 ဖြစ်နေပါမယ်။ (configFileContent ကနေ အစားထိုး ဝင်သွားတာပါ)// finalAppConfig.apiBaseUrl ဟာ "/api/v2" ဖြစ်နေပါမယ်။// finalAppConfig.featureFlags ဟာ { betaFeature: true, logging: true } လို့ ဖြစ်နေမှာ မဟုတ်ဘဲ { betaFeature: true } ပဲ ဖြစ်နေပါမယ်။// ⚠️ သတိပြုရန်: Spread Operator (...) ကို သုံးပြီး Shallow Merge လုပ်ထားတာ ဖြစ်တဲ့အတွက်၊ Nested Object တွေ (ဥပမာ featureFlags) ကို ပေါင်းစပ်တဲ့အခါ အစားထိုး ဖုံးအုပ်သွားတာမျိုး ဖြစ်တတ်ပါတယ်။ Deep Merge မဟုတ်ပါဘူး။
// Type-safe ဖြစ်ဖြစ် ယုံကြည်မှုရှိရှိနဲ့ ခေါ်ယူ အသုံးပြုနိုင်ပါပြီ:console.log("App Environment:", finalAppConfig.env);console.log("API Base URL:", finalAppConfig.apiBaseUrl.toUpperCase());if (finalAppConfig.featureFlags?.betaFeature) { console.log("Beta Feature ကို ဖွင့်ထားပါတယ်!");}
// finalAppConfig.port = 3001;// 👆 Error တက်ပါမယ်! Readonly<AppConfig> ဖြစ်နေတဲ့အတွက် ပြင်ဆင်လို့ မရပါဘူး။
// const customUrl = finalAppConfig.customServiceUrl;// 👆 Error တက်ပါမယ်! 'customServiceUrl' ဟာ AppConfig ရဲ့ Type Definition ထဲမှာ မပါဝင်ပါဘူး ခင်ဗျာ။satisfies အသုံးပြုပုံ ရှင်းပြချက်:
Section titled “satisfies အသုံးပြုပုံ ရှင်းပြချက်:”configFileContentကို ဖန်တီးတဲ့အခါsatisfies Partial<AppConfig>နှင့်{ customServiceUrl?: string }လို့ အဆုံးမှာ ထည့်ရေးထားပါတယ်။ ဒါဟာconfigFileContentအထဲမှာ ရေးထားတဲ့ Data တွေကPartial<AppConfig>ရဲ့ စည်းမျဉ်းတွေနဲ့ တကယ် ကိုက်ညီရဲ့လား ဆိုတာကို TypeScript ကို စစ်ဆေးခိုင်းလိုက်တာပါ။- တစ်ပြိုင်နက်တည်းမှာပဲ
AppConfigရဲ့ မူလ ဒီဇိုင်းမှာ မပါဝင်တဲ့customServiceUrlလိုမျိုး အပို Properties တွေကိုလည်း (လိုအပ်ရင် ထည့်လို့ရအောင်) ’&’ သုံးပြီး ခွင့်ပြုပေးထားတာ ဖြစ်ပါတယ် ခင်ဗျာ။