1 पॉइंट द्वारा GN⁺ 4 시간 전 | 1 टिप्पणियां | WhatsApp पर शेयर करें
  • अगर TypeScript कोड में if (user.email) जैसी जाँचें बिखर जाती हैं, तो जो तथ्य पहले ही जाँचा जा चुका है वह type में नहीं बचता, इसलिए call stack के बाद के हिस्सों में उसी शर्त पर बार-बार संदेह होता रहता है
  • parser raw input लेकर ज़्यादा संकीर्ण type या failure information लौटाता है, जिससे EmailAddress जैसे सत्यापित तथ्य पर प्रोग्राम का बाकी हिस्सा भरोसा कर सकता है
  • structural type system इस्तेमाल करने वाले TypeScript में string और Email स्वाभाविक रूप से अलग नहीं होते, इसलिए unique symbol आधारित branded types और सीमित as assertion से nominal boundary की नकल की जाती है
  • Parsed<T> जैसे discriminated union success और failure को type signature में दिखाते हैं, लेकिन dedicated match expression न होने से never का इस्तेमाल कर exhaustive check खुद लिखना पड़ता है
  • Zod, io-ts, valibot schema से parser और TypeScript type साथ बना सकते हैं, लेकिन external input को domain type मानने से पहले हर boundary पर parsing करने का अनुशासन अब भी developer पर ही रहता है

वैलिडेशन जानकारी फेंक देता है, parsing उसे type में बचाकर रखती है

  • Alexis King के Parse, don’t validate सिद्धांत का केंद्र validator और parser का फर्क है
    • validator यह तय करता है कि “यह value ठीक है” और फिर boolean या exception के जरिए flow आगे बढ़ा देता है
    • parser raw input लेकर उससे ज़्यादा सटीक type बनाता है या failure की वजह लौटाता है
  • अगर User.email: string, User.age: number की तरह type चौड़े ही बने रहें, तो isValidUser(user): boolean पास होने पर भी TypeScript इस तथ्य को याद नहीं रख पाता
  • बाद में emailService.send(user.email, ...) जैसे कोड में user.email अब भी खाली string, "hello", "definitely not an email" जैसी साधारण string ही रहती है
  • एक ही शर्त को कई जगह दोबारा जाँचने वाला flow, King के कहे shotgun parsing के क़रीब है

जब type खुद सबूत बन जाए, ऐसा API

  • वांछित रूप sendWelcome(user: ValidUser) जैसी function signature है, जो सिर्फ parsed value ही स्वीकार करे
  • इस संरचना में sendWelcome को call करने से पहले parser से गुजरना अनिवार्य है, और function के भीतर अलग से re-validation या defensive if की ज़रूरत नहीं पड़ती
  • Elm में opaque type और smart constructor से यह आसानी से हो जाता है, लेकिन TypeScript में वही असर पाने के लिए ज़्यादा साधन लगाने पड़ते हैं

branded types से nominal boundary बनाना

  • TypeScript structural type system इस्तेमाल करता है, इसलिए एक ही shape वाले type को एक ही type माना जाता है
    • string तो string ही है, और Haskell के newtype जैसी सचमुच अलग type बनाने की सुविधा नहीं है
  • community में इस्तेमाल होने वाला workaround है branding या tagging
    • सरल तरीका है { readonly __brand: "Email" } जैसी string literal phantom field
    • ज़्यादा मज़बूत तरीका है module के बाहर export न किया गया unique symbol brand key के रूप में इस्तेमाल करना
  • उदाहरण type का रूप type Email = string & { readonly [EmailBrand]: true }, type Age = number & { readonly [AgeBrand]: true } है
  • brand field runtime में मौजूद नहीं होती, यह type-level marker है जो compile time पर Email और string को अलग तरह से treat कराती है
  • brand सिर्फ एक दिशा में काम करता है
    • Email, string को assign किया जा सकता है
    • सामान्य string सीधे Email में नहीं आ सकती

parser भरोसे की boundary पर ही assertion की अनुमति देता है

  • parseEmail(raw: string): Parsed<Email> string में @ न होने पर failure लौटाता है, और पास होने पर raw as Email से branded type बनाता है
  • as Email assertion इसलिए स्वीकार्य अपवाद है क्योंकि parser ही trust boundary है
    • अगर codebase के किसी और हिस्से में string को Email मानकर assert किया जाए तो design टूट जाता है
    • parser को अलग module में रखकर, उसके बाहर brand assertion दिखे तो उसे bug माना जा सकता है
  • उदाहरण का Parsed<T> रूप { kind: "ok"; value: T } | { kind: "err"; error: ParseError } है
    • failure exception के रूप में छिपी नहीं रहती, बल्कि type signature में दिखती है
    • kind: "ok" | "err" जैसे string discriminator इस्तेमाल करने पर बाद में variant बढ़ने पर type narrowing ज़्यादा ईमानदारी से काम करती है
  • parseEmail का उदाहरण जानबूझकर हल्का रखा गया है; असली email parser को trim, lowercase, domain validation जैसी और चीज़ें भी संभालनी होंगी

raw input और trusted domain type को अलग रखना

  • UnvalidatedUser और ValidUser को अलग करने से network या external input से आए value और domain में भरोसेमंद value के बीच साफ़ अंतर हो जाता है
    • UnvalidatedUser में id, email, age को unknown रखा जाता है
    • ValidUser में UserId, Email, Age जैसे branded type इस्तेमाल होते हैं
  • UserId को भी brand करने पर UserId चाहिए जहाँ वहाँ गलती से OrderId जैसा कोई और ID देने से बचा जा सकता है
  • parseUser(raw: unknown): Parsed<ValidUser> raw input को चरणबद्ध तरीके से संकीर्ण करता है
    • पहले जाँचता है कि input object है या नहीं
    • फिर id, email, age field की मौजूदगी जाँचता है
    • email string है या नहीं, यह जाँचता है
    • parseUserId, parseEmail, parseAge को अलग-अलग call करता है और failure होने पर तुरंत लौट जाता है
    • सब सफल हों तो ValidUser लौटाता है
  • यह तरीका F# या Elm से ज़्यादा verbose है, लेकिन sendWelcome(user: ValidUser) को वास्तव में सुरक्षित बना देता है

TypeScript जहाँ चुभता है

  • पहली friction parser के भीतर as Email assertion है
    • असली nominal type language में smart constructor बिना झूठ बोले नया type लौटा सकता है
    • TypeScript का brand एक काल्पनिक type marker है, इसलिए parser को assertion के सहारे आगे बढ़ना पड़ता है
  • दूसरी friction exhaustive check है
    • TypeScript का discriminated union इस शैली में बहुत ताकतवर है, लेकिन dedicated match expression नहीं है
    • switch के default में const _exhaustive: never = result जैसा pattern खुद लिखना पड़ता है
    • अगर Parsed में तीसरा variant जुड़ जाए, तो never assignment fail होगी और compiler जगह बता देगा
  • satisfies को cast से ज़्यादा शालीन escape hatch की तरह इस्तेमाल किया जा सकता है
    • const x = { ... } satisfies Config type को जाँचता भी है और literal type को बेवजह चौड़ा भी नहीं करता
  • JSON.parse any लौटाता है, इसलिए उसे तुरंत unknown के रूप में annotate करना ज़्यादा सुरक्षित है
    • const raw: unknown = JSON.parse(input) के रूप में लें, फिर parser तय करे कि यह domain type है या नहीं
    • JSON.parse validator नहीं, बल्कि bytes को JS value में बदलने वाला deserialization चरण है

Zod जैसी libraries जो दोहराव कम करती हैं

  • Zod, io-ts, valibot हाथ से लिखे parser की तुलना में यही pattern ज़्यादा सुविधाजनक ढंग से देती हैं
  • Zod का उदाहरण एक ही schema से parser और TypeScript type दोनों बनाता है
    • z.object({ id: z.number().int(), email: z.string().email().brand<"Email">(), age: z.number().int().min(0).max(150).brand<"Age">() })
    • z.infer<typeof ValidUserSchema> से type मिलता है
    • ValidUserSchema.safeParse(rawInput) success पर data और failure पर error लौटाता है
  • Zod का .brand() भी हाथ से बनाए symbol brand की तरह type-level feature है; इसका runtime behavior नहीं होता
  • library parser और type को एक ही definition में बाँधकर boundary बनाए रखना आसान करती है, लेकिन हर external boundary पर इसे इस्तेमाल करना है, यह अनुशासन अपने आप लागू नहीं करती
  • network से आया User, parse होने तक domain User नहीं है, और error message से बचने के लिए type assertion का शॉर्टकट लेने के प्रलोभन से बचना चाहिए

सबूत को याददाश्त में नहीं, type में रखो

  • छोटा-सा सिद्धांत यह है कि “type system को सबूत साथ रखने दो, इसे इंसानी याददाश्त पर मत छोड़ो”
  • अगर किसी शर्त को जाँचकर उसका नतीजा type में encode नहीं किया गया, तो बाद का code आसानी से मान लेता है कि validation पहले ही हो चुकी है
  • TypeScript में यह सिद्धांत तीन औज़ारों के सहारे लागू होता है
    • nominal identity की नकल करने वाले branded types
    • success और failure को दिखाने वाले discriminated union
    • external input के unknown और trusted domain type के बीच सख्त boundary
  • हर code को parsing pipeline में बदलना हमेशा सही नहीं होता, लेकिन अगर वही defensive if कई files में दोहराया जा रहा है, तो यह संकेत है कि जिस जानकारी को validate करना चाहिए था वह type में समाई नहीं है

1 टिप्पणियां

 
GN⁺ 4 시간 전
Lobste.rs की राय
  • अगर JavaScript/TypeScript जिस code style को बढ़ावा देता है, उससे तकनीकी या ergonomics के स्तर पर टकराव है, तो JS में compile होने वाली कई भाषाओं में से कोई एक क्यों न इस्तेमाल करें
    Haskell, Elm, F# का ज़िक्र हुआ, और PureScript, js_of_ocaml, Reason, LunarML जैसी कई भाषाएँ भी हैं जिन्हें लेखक और अधिक इस्तेमाल करना चाहता है। लेखक ने Why TypeScript Won’t Save You नाम का लेख भी लिखा है, जहाँ उसने अपनी पसंदीदा भाषाओं से और तुलना की है, और https://learnelm.dev भी चलाता है।
    या फिर शायद तुलना करना ही मकसद है, ताकि दिखाया जा सके कि TypeScript कई मामलों में पर्याप्त नहीं है और लोग दूसरे toolchain या ideas अपनाने पर सोचें

    • मौजूदा codebase, टीम की किसी खास भाषा में skill, कंपनी की guidelines, और कम support·tools·community size जैसी सीमाएँ होती हैं
      ज़्यादातर लोगों के पास बस कोई दूसरी भाषा चुनने का विकल्प या समय ही नहीं होता
    • आम तौर पर वजह यह होगी कि उनके पास बड़ा TypeScript codebase है, या वे ऐसी TypeScript library इस्तेमाल करते हैं जो दूसरी भाषाओं में उपलब्ध नहीं है
  • काम में मुझे branded type बहुत पसंद हैं, लेकिन यह बात सच में खटकती है कि branded numbers से ही index होने वाला Array या TypedArray बनाया नहीं जा सकता
    TypedArray में branded numbers को store करना, या और सटीक कहें तो उन्हें निकालकर पढ़ना भी संभव नहीं है। भले इसके लिए IndexArray या IndexTypedArray जैसे अलग type set की ज़रूरत पड़े, फिर भी ऐसा feature ज़रूर होना चाहिए

    • मुझे भी branded types पसंद हैं, लेकिन बात करने पर अक्सर लोग मानते हैं कि जितनी मेहनत लगती है, उसके हिसाब से फायदा कम है
      लेकिन काफ़ी जटिल database schema में अगर हर ID पर branded type लगाया जाए, तो TypeScript बेकार join या condition पकड़ लेता है। function signatures भी ज़्यादा स्पष्ट हो जाते हैं और कई तरह की गलतियाँ करना कठिन हो जाता है
    • अगर आप काफ़ी ज़ोर से झूठ बोलने को तैयार हों, तो branded numbers से ही index होने वाला Array बनाया जा सकता है
      चाहें तो TypedArray के values के लिए भी यही तरीका अपनाया जा सकता है
    • नौकरी में हम “smart enum” और custom array types इस्तेमाल करते हैं, जिससे TArray<Foo, MyEnum> जैसा लिखा जा सकता है। लेकिन यह C++ की बात है
      Zig की std library में comptime से बना EnumArray है। यह dense enum और sparse enum दोनों को indexing के लिए इस्तेमाल कर सकता है, compile time पर सही indexer निकाल सकता है, और इससे भी व्यापक features देता है।
      इस तरह की precision typing मुझे अब लगातार ज़्यादा पसंद आ रही है। यह codebase में logical bugs के घुसने को काफ़ी हद तक रोकती है