প্রায় প্রতিটি ওয়েব অ্যাপ্লিকেশনের ডেটা সিরিয়ালাইজেশন প্রয়োজন। এই প্রয়োজনটি এমন পরিস্থিতিতে দেখা দেয়:
অনেক ক্ষেত্রে, ডেটা হারানো বা দুর্নীতি গুরুতর পরিণতির দিকে নিয়ে যেতে পারে, এটি একটি সুবিধাজনক এবং নিরাপদ সিরিয়ালাইজেশন মেকানিজম প্রদান করা অপরিহার্য করে তোলে যা বিকাশের পর্যায়ে যতটা সম্ভব ত্রুটি সনাক্ত করতে সহায়তা করে। এই উদ্দেশ্যে, ডেভেলপমেন্টের সময় স্ট্যাটিক কোড চেক করার জন্য ডেটা ট্রান্সফার ফর্ম্যাট এবং টাইপস্ক্রিপ্ট হিসাবে JSON ব্যবহার করা সুবিধাজনক।
টাইপস্ক্রিপ্ট জাভাস্ক্রিপ্টের একটি সুপারসেট হিসাবে কাজ করে, যা JSON.stringify
এবং JSON.parse
মতো ফাংশনগুলির নিরবচ্ছিন্ন ব্যবহারকে সক্ষম করবে, তাই না? দেখা যাচ্ছে, এর সমস্ত সুবিধা থাকা সত্ত্বেও, TypeScript স্বাভাবিকভাবেই বুঝতে পারে না JSON কী এবং কোন ডেটা প্রকারগুলি JSON-এ সিরিয়ালাইজেশন এবং ডিসিরিয়ালাইজেশনের জন্য নিরাপদ।
একটি উদাহরণ দিয়ে এটি ব্যাখ্যা করা যাক।
উদাহরণস্বরূপ, একটি ফাংশন বিবেচনা করুন যা কিছু ডেটা LocalStorage এ সংরক্ষণ করে। যেহেতু LocalStorage বস্তুগুলি সংরক্ষণ করতে পারে না, আমরা এখানে JSON সিরিয়ালাইজেশন ব্যবহার করি:
interface PostComment { authorId: string; text: string; updatedAt: Date; } function saveComment(comment: PostComment) { const serializedComment = JSON.stringify(comment); localStorage.setItem('draft', serializedComment); }
LocalStorage থেকে ডেটা পুনরুদ্ধার করার জন্য আমাদের একটি ফাংশনের প্রয়োজন হবে।
function restoreComment(): PostComment | undefined { const text = localStorage.getItem('draft'); return text ? JSON.parse(text) : undefined; }
এই কোডের সাথে ভুল কি? প্রথম সমস্যাটি হল যে মন্তব্যটি পুনরুদ্ধার করার সময়, আমরা updatedAt
ক্ষেত্রের জন্য Date
পরিবর্তে একটি string
টাইপ পাব।
এটি ঘটে কারণ JSON-এর শুধুমাত্র চারটি আদিম ডেটা প্রকার ( null
, string
, number
, boolean
), পাশাপাশি অ্যারে এবং অবজেক্ট রয়েছে৷ JSON-এ একটি Date
অবজেক্ট সংরক্ষণ করা সম্ভব নয়, সেইসাথে জাভাস্ক্রিপ্টে পাওয়া অন্যান্য অবজেক্ট: ফাংশন, ম্যাপ, সেট ইত্যাদি।
যখন JSON.stringify
একটি মানের সম্মুখীন হয় যা JSON ফর্ম্যাটে উপস্থাপন করা যায় না, টাইপ কাস্টিং ঘটে। একটি Date
অবজেক্টের ক্ষেত্রে, আমরা একটি স্ট্রিং পাই কারণ Date
অবজেক্ট toJson() পদ্ধতি প্রয়োগ করে, যা একটি Date
অবজেক্টের পরিবর্তে একটি স্ট্রিং প্রদান করে।
const date = new Date('August 19, 1975 23:15:30 UTC'); const jsonDate = date.toJSON(); console.log(jsonDate); // Expected output: "1975-08-19T23:15:30.000Z" const isEqual = date.toJSON() === JSON.stringify(date); console.log(isEqual); // Expected output: true
দ্বিতীয় সমস্যা হল saveComment
ফাংশন PostComment
টাইপ রিটার্ন করে, যেখানে date ক্ষেত্রটি Date
টাইপ। কিন্তু আমরা ইতিমধ্যেই জানি যে Date
এর পরিবর্তে আমরা একটি string
টাইপ পাব। TypeScript আমাদের এই ত্রুটি খুঁজে পেতে সাহায্য করতে পারে, কিন্তু কেন এটি নয়?
দেখা যাচ্ছে, TypeScript এর স্ট্যান্ডার্ড লাইব্রেরিতে, JSON.parse
ফাংশনটি (text: string) => any
হিসাবে টাইপ করা হয়েছে। any
ব্যবহারের কারণে, টাইপ চেকিং মূলত অক্ষম। আমাদের উদাহরণে, TypeScript সহজভাবে আমাদের শব্দটি নিয়েছে যে ফাংশনটি একটি Date
অবজেক্ট ধারণকারী একটি PostComment
প্রদান করবে।
এই TypeScript আচরণ অসুবিধাজনক এবং অনিরাপদ। আমাদের অ্যাপ্লিকেশন ক্র্যাশ হতে পারে যদি আমরা একটি স্ট্রিংকে Date
অবজেক্টের মতো আচরণ করার চেষ্টা করি। উদাহরণস্বরূপ, আমরা comment.updatedAt.toLocaleDateString()
কল করলে এটি ভেঙে যেতে পারে।
প্রকৃতপক্ষে, আমাদের ছোট উদাহরণে, আমরা কেবলমাত্র Date
অবজেক্টটিকে একটি সংখ্যাসূচক টাইমস্ট্যাম্প দিয়ে প্রতিস্থাপন করতে পারি, যা JSON সিরিয়ালাইজেশনের জন্য ভাল কাজ করে। যাইহোক, বাস্তব অ্যাপ্লিকেশনগুলিতে, ডেটা অবজেক্টগুলি বিস্তৃত হতে পারে, প্রকারগুলি একাধিক স্থানে সংজ্ঞায়িত করা যেতে পারে এবং বিকাশের সময় এই জাতীয় ত্রুটি সনাক্ত করা একটি চ্যালেঞ্জিং কাজ হতে পারে।
আমরা যদি JSON এর TypeScript এর বোঝার উন্নতি করতে পারি?
শুরু করার জন্য, আসুন জেনে নেওয়া যাক কীভাবে টাইপস্ক্রিপ্ট বোঝা যায় যে কোন ডেটা প্রকারগুলিকে নিরাপদে JSON-এ সিরিয়ালাইজ করা যায়। ধরুন আমরা একটি ফাংশন safeJsonStringify
তৈরি করতে চাই, যেখানে TypeScript ইনপুট ডেটা ফরম্যাটটি JSON সিরিয়ালাইজেবল নিশ্চিত করতে পরীক্ষা করবে।
function safeJsonStringify(data: JSONValue) { return JSON.stringify(data); }
এই ফাংশনে, সবচেয়ে গুরুত্বপূর্ণ অংশটি হল JSONValue
টাইপ, যা JSON ফর্ম্যাটে উপস্থাপন করা যেতে পারে এমন সমস্ত সম্ভাব্য মানগুলিকে উপস্থাপন করে। বাস্তবায়ন বেশ সহজবোধ্য:
type JSONPrimitive = string | number | boolean | null | undefined; type JSONValue = JSONPrimitive | JSONValue[] | { [key: string]: JSONValue; };
প্রথমত, আমরা JSONPrimitive
টাইপ সংজ্ঞায়িত করি, যা সমস্ত আদিম JSON ডেটা প্রকার বর্ণনা করে। আমরা undefined
প্রকারটিও অন্তর্ভুক্ত করি এই সত্যের উপর ভিত্তি করে যে যখন ক্রমিক করা হয়, তখন undefined
মান সহ কীগুলি বাদ দেওয়া হবে। ডিসিরিয়ালাইজেশনের সময়, এই কীগুলি বস্তুতে উপস্থিত হবে না, যা বেশিরভাগ ক্ষেত্রে একই জিনিস।
পরবর্তী, আমরা JSONValue
প্রকার বর্ণনা করি। এই টাইপটি টাইপস্ক্রিপ্টের রিকার্সিভ টাইপ বর্ণনা করার ক্ষমতা ব্যবহার করে, যেগুলো নিজেদেরকে বোঝায়। এখানে, JSONValue
হয় একটি JSONPrimitive
, JSONValue
এর একটি অ্যারে বা একটি বস্তু হতে পারে যেখানে সমস্ত মান JSONValue
ধরনের। ফলস্বরূপ, এই ধরনের JSONValue
এর একটি ভেরিয়েবলে সীমাহীন নেস্টিং সহ অ্যারে এবং অবজেক্ট থাকতে পারে। এর মধ্যে থাকা মানগুলিও JSON ফর্ম্যাটের সাথে সামঞ্জস্যের জন্য পরীক্ষা করা হবে।
এখন আমরা নিম্নলিখিত উদাহরণ ব্যবহার করে আমাদের safeJsonStringify
ফাংশন পরীক্ষা করতে পারি:
// No errors safeJsonStringify({ updatedAt: Date.now() }); // Yields an error: // Argument of type '{ updatedAt: Date; }' is not assignable to parameter of type 'JSONValue'. // Types of property 'updatedAt' are incompatible. // Type 'Date' is not assignable to type 'JSONValue'. safeJsonStringify({ updatedAt: new Date(); });
সবকিছু সঠিকভাবে কাজ করছে বলে মনে হচ্ছে। ফাংশনটি আমাদেরকে একটি সংখ্যা হিসাবে তারিখটি পাস করার অনুমতি দেয় কিন্তু যদি আমরা Date
অবজেক্টটি পাস করি তবে একটি ত্রুটি দেখা দেয়।
তবে আসুন একটি আরও বাস্তবসম্মত উদাহরণ বিবেচনা করি, যেখানে ফাংশনে পাস করা ডেটা একটি ভেরিয়েবলে সংরক্ষণ করা হয় এবং একটি বর্ণিত প্রকার রয়েছে।
interface PostComment { authorId: string; text: string; updatedAt: number; }; const comment: PostComment = {...}; // Yields an error: // Argument of type 'PostComment' is not assignable to parameter of type 'JSONValue'. // Type 'PostComment' is not assignable to type '{ [key: string]: JSONValue; }'. // Index signature for type 'string' is missing in type 'PostComment'. safeJsonStringify(comment);
এখন, জিনিসগুলি কিছুটা জটিল হচ্ছে। TypeScript আমাদের JSONValue
টাইপের একটি ফাংশন প্যারামিটারে PostComment
টাইপের একটি ভেরিয়েবল বরাদ্দ করতে দেবে না, কারণ "টাইপ 'স্ট্রিং'-এর জন্য সূচক স্বাক্ষর 'পোস্টকমেন্ট' টাইপে অনুপস্থিত"।
সুতরাং, একটি সূচক স্বাক্ষর কি এবং কেন এটি অনুপস্থিত? মনে রাখবেন যে আমরা JSON ফর্ম্যাটে সিরিয়াল করা যেতে পারে এমন বস্তুগুলিকে কীভাবে বর্ণনা করেছি?
type JSONValue = { [key: string]: JSONValue; };
এই ক্ষেত্রে, [key: string]
হল সূচক স্বাক্ষর। এর অর্থ হল "এই বস্তুর স্ট্রিং আকারে যেকোনো কী থাকতে পারে, যার মান JSONValue
টাইপ আছে"। সুতরাং, দেখা যাচ্ছে আমাদের PostComment
টাইপের একটি সূচক স্বাক্ষর যোগ করতে হবে, তাই না?
interface PostComment { authorId: string; text: string; updatedAt: number; // Don't do this: [key: string]: JSONValue; };
এটি করলে বোঝাবে যে মন্তব্যটিতে যেকোনো অবাধ ক্ষেত্র থাকতে পারে, যা একটি অ্যাপ্লিকেশনে ডেটা টাইপ সংজ্ঞায়িত করার সময় সাধারণত কাঙ্ক্ষিত ফলাফল হয় না।
সূচী স্বাক্ষরের সমস্যাটির আসল সমাধানটি আসে ম্যাপড টাইপস থেকে, যা ক্ষেত্রগুলিতে পুনরাবৃত্তভাবে পুনরাবৃত্তি করার অনুমতি দেয়, এমনকি যে ধরনের সূচক স্বাক্ষর সংজ্ঞায়িত নেই তাদের জন্যও। জেনেরিকের সাথে মিলিত, এই বৈশিষ্ট্যটি যেকোনও ডেটা টাইপ T
অন্য প্রকার JSONCompatible<T>
এ রূপান্তর করতে দেয়, যা JSON ফর্ম্যাটের সাথে সামঞ্জস্যপূর্ণ।
type JSONCompatible<T> = unknown extends T ? never : { [P in keyof T]: T[P] extends JSONValue ? T[P] : T[P] extends NotAssignableToJson ? never : JSONCompatible<T[P]>; }; type NotAssignableToJson = | bigint | symbol | Function;
JSONCompatible<T>
টাইপ হল একটি ম্যাপ করা টাইপ যা পরিদর্শন করে যে প্রদত্ত টাইপ T
নিরাপদে JSON-এ সিরিয়ালাইজ করা যায় কিনা। এটি T
টাইপের প্রতিটি সম্পত্তির উপর পুনরাবৃত্তি করে এবং নিম্নলিখিতগুলি করে এটি করে:
T[P] extends JSONValue ? T[P] : ...
JSONValue
যখন এটি হয়, সম্পত্তির ধরন অপরিবর্তিত থাকে।T[P] extends NotAssignableToJson ? never : ...
শর্তসাপেক্ষ টাইপ যাচাই করে যদি সম্পত্তির ধরন JSON-এর জন্য বরাদ্দযোগ্য না হয়। এই ক্ষেত্রে, সম্পত্তির ধরনকে never
তে রূপান্তরিত করা হয়, কার্যকরভাবে সম্পত্তিটিকে চূড়ান্ত প্রকার থেকে ফিল্টার করে।
unknown extends T ? never :...
শুরুতে চেক unknown
টাইপটিকে একটি খালি অবজেক্ট টাইপ {}
-এ রূপান্তরিত হতে বাধা দিতে ব্যবহার করা হয়, যা মূলত any
ধরনের সমতুল্য।
আরেকটি আকর্ষণীয় দিক হল NotAssignableToJson
প্রকার। এটি দুটি টাইপস্ক্রিপ্ট আদিম (বিজিন্ট এবং প্রতীক) এবং Function
টাইপ নিয়ে গঠিত, যা যেকোনো সম্ভাব্য ফাংশন বর্ণনা করে। JSON-এর জন্য বরাদ্দযোগ্য নয় এমন কোনও মান ফিল্টার করার ক্ষেত্রে Function
ধরন অত্যন্ত গুরুত্বপূর্ণ। এর কারণ হল জাভাস্ক্রিপ্টের যেকোনো জটিল বস্তু অবজেক্টের প্রকারের উপর ভিত্তি করে এবং এর প্রোটোটাইপ চেইনে অন্তত একটি ফাংশন থাকে (যেমন, toString()
)। JSONCompatible
টাইপ সেই সমস্ত ফাংশনগুলির উপর পুনরাবৃত্তি করে, তাই JSON-এর জন্য সিরিয়ালাইজযোগ্য নয় এমন কিছু ফিল্টার করার জন্য ফাংশন চেক করা যথেষ্ট।
এখন, সিরিয়ালাইজেশন ফাংশনে এই ধরনের ব্যবহার করা যাক:
function safeJsonStringify<T>(data: JSONCompatible<T>) { return JSON.stringify(data); }
এখন, ফাংশনটি একটি জেনেরিক প্যারামিটার T
ব্যবহার করে এবং JSONCompatible<T>
আর্গুমেন্ট গ্রহণ করে। এর মানে এটি T
টাইপের একটি আর্গুমেন্ট data
নেয়, যা একটি JSON- সামঞ্জস্যপূর্ণ টাইপ হওয়া উচিত। এখন আমরা একটি সূচক স্বাক্ষর ছাড়াই ডেটা প্রকারের সাথে ফাংশনটি ব্যবহার করতে পারি।
ফাংশনটি এখন একটি জেনেরিক প্যারামিটার T
ব্যবহার করে যা JSONCompatible<T>
প্রকার থেকে প্রসারিত হয়। এর মানে হল যে এটি T
টাইপের একটি আর্গুমেন্ট data
গ্রহণ করে, যা একটি JSON- সামঞ্জস্যপূর্ণ টাইপ হওয়া উচিত। ফলস্বরূপ, আমরা ডেটা প্রকারের সাথে ফাংশনটি ব্যবহার করতে পারি যার একটি সূচক স্বাক্ষর নেই।
interface PostComment { authorId: string; text: string; updatedAt: number; } function saveComment(comment: PostComment) { const serializedComment = safeJsonStringify(comment); localStorage.setItem('draft', serializedComment); }
এই পদ্ধতিটি ব্যবহার করা যেতে পারে যখনই JSON সিরিয়ালাইজেশনের প্রয়োজন হয়, যেমন নেটওয়ার্কে ডেটা স্থানান্তর করা, HTML এ ডেটা এম্বেড করা, লোকাল স্টোরেজে ডেটা সঞ্চয় করা, কর্মীদের মধ্যে ডেটা স্থানান্তর করা ইত্যাদি। উপরন্তু, toJsonValue
সাহায্যকারী ব্যবহার করা যেতে পারে যখন কোনও কঠোরভাবে টাইপ করা বস্তু ছাড়া JSONValue
প্রকারের একটি ভেরিয়েবলের জন্য একটি সূচক স্বাক্ষর বরাদ্দ করা প্রয়োজন৷
function toJsonValue<T>(value: JSONCompatible<T>): JSONValue { return value; } const comment: PostComment = {...}; const data: JSONValue = { comment: toJsonValue(comment) };
এই উদাহরণে, toJsonValue
ব্যবহার করে PostComment
টাইপের অনুপস্থিত সূচক স্বাক্ষর সম্পর্কিত ত্রুটিটি বাইপাস করতে দেয়।
যখন ডিসিরিয়ালাইজেশন আসে, চ্যালেঞ্জটি একই সাথে সহজ এবং জটিল উভয়ই কারণ এতে প্রাপ্ত ডেটার বিন্যাসের জন্য স্ট্যাটিক বিশ্লেষণ চেক এবং রানটাইম চেক উভয়ই জড়িত।
TypeScript এর টাইপ সিস্টেমের দৃষ্টিকোণ থেকে, চ্যালেঞ্জটি বেশ সহজ। আসুন নিম্নলিখিত উদাহরণ বিবেচনা করা যাক:
function safeJsonParse(text: string) { return JSON.parse(text) as unknown; } const data = JSON.parse(text); // ^? unknown
এই উদাহরণে, আমরা unknown
টাইপের সাথে any
রিটার্ন টাইপ প্রতিস্থাপন করছি। কেন unknown
চয়ন? মূলত, একটি JSON স্ট্রিং-এ যেকোন কিছু থাকতে পারে, শুধু যে ডেটা আমরা পাওয়ার আশা করি তা নয়। উদাহরণস্বরূপ, বিভিন্ন অ্যাপ্লিকেশন সংস্করণের মধ্যে ডেটা বিন্যাস পরিবর্তিত হতে পারে বা অ্যাপের অন্য অংশ একই লোকালস্টোরেজ কীতে ডেটা লিখতে পারে। অতএব, unknown
সবচেয়ে নিরাপদ এবং সবচেয়ে সুনির্দিষ্ট পছন্দ।
যাইহোক, unknown
প্রকারের সাথে কাজ করা শুধুমাত্র পছন্দসই ডেটা টাইপ নির্দিষ্ট করার চেয়ে কম সুবিধাজনক। টাইপ-কাস্টিং ছাড়াও, unknown
টাইপকে প্রয়োজনীয় ডেটা টাইপে রূপান্তর করার একাধিক উপায় রয়েছে। এই ধরনের একটি পদ্ধতি রানটাইমে ডেটা যাচাই করতে সুপারস্ট্রাক্ট লাইব্রেরি ব্যবহার করে এবং ডেটা অবৈধ হলে বিস্তারিত ত্রুটি নিক্ষেপ করে।
import { create, object, number, string } from 'superstruct'; const PostComment = object({ authorId: string(), text: string(), updatedAt: number(), }); // Note: we no longer need to manually specify the return type function restoreDraft() { const text = localStorage.getItem('draft'); return text ? create(JSON.parse(text), PostComment) : undefined; }
এখানে, create
ফাংশনটি টাইপ গার্ড হিসাবে কাজ করে, টাইপটিকে পছন্দসই Comment
ইন্টারফেসে সংকুচিত করে । ফলস্বরূপ, আমাদের আর ম্যানুয়ালি রিটার্ন টাইপ নির্দিষ্ট করতে হবে না।
একটি নিরাপদ ডিসিরিয়ালাইজেশন বিকল্প বাস্তবায়ন করা মাত্র অর্ধেক গল্প। প্রকল্পের পরবর্তী কাজটি মোকাবেলা করার সময় এটি ব্যবহার করতে ভুলবেন না সমানভাবে গুরুত্বপূর্ণ। এটি বিশেষভাবে চ্যালেঞ্জিং হয়ে ওঠে যদি একটি বড় দল প্রকল্পে কাজ করে, কারণ সমস্ত চুক্তি এবং সর্বোত্তম অনুশীলনগুলি অনুসরণ করা হয় তা নিশ্চিত করা কঠিন হতে পারে।
Typescript-eslint এই কাজে সহায়তা করতে পারে। এই টুলটি any
ব্যবহারের অনিরাপদ সব ঘটনা শনাক্ত করতে সাহায্য করে। বিশেষত, JSON.parse
এর সমস্ত ব্যবহার পাওয়া যাবে এবং প্রাপ্ত ডেটার বিন্যাস চেক করা হয়েছে তা নিশ্চিত করা যেতে পারে। কোডবেসের any
প্রকার থেকে পরিত্রাণ পাওয়ার বিষয়ে আরও পড়া যাবে টাইপস্ক্রিপ্ট মেকিং ট্রুলি "স্ট্রংলি টাইপড" নিবন্ধে।
নিরাপদ JSON সিরিয়ালাইজেশন এবং ডিসিরিয়ালাইজেশনে সহায়তা করার জন্য ডিজাইন করা চূড়ান্ত ইউটিলিটি ফাংশন এবং প্রকারগুলি এখানে রয়েছে৷ আপনি প্রস্তুত টিএস খেলার মাঠে এগুলি পরীক্ষা করতে পারেন।
type JSONPrimitive = string | number | boolean | null | undefined; type JSONValue = JSONPrimitive | JSONValue[] | { [key: string]: JSONValue; }; type NotAssignableToJson = | bigint | symbol | Function; type JSONCompatible<T> = unknown extends T ? never : { [P in keyof T]: T[P] extends JSONValue ? T[P] : T[P] extends NotAssignableToJson ? never : JSONCompatible<T[P]>; }; function toJsonValue<T>(value: JSONCompatible<T>): JSONValue { return value; } function safeJsonStringify<T>(data: JSONCompatible<T>) { return JSON.stringify(data); } function safeJsonParse(text: string): unknown { return JSON.parse(text); }
এগুলি যে কোনও পরিস্থিতিতে ব্যবহার করা যেতে পারে যেখানে JSON সিরিয়ালাইজেশন প্রয়োজনীয়।
আমি এখন বেশ কয়েক বছর ধরে আমার প্রকল্পগুলিতে এই কৌশলটি ব্যবহার করছি, এবং এটি অ্যাপ্লিকেশন বিকাশের সময় সম্ভাব্য ত্রুটিগুলি অবিলম্বে সনাক্ত করে এর কার্যকারিতা প্রদর্শন করেছে।
আমি আশা করি এই নিবন্ধটি আপনাকে কিছু নতুন অন্তর্দৃষ্টি প্রদান করেছে। পড়ার জন্য আপনাকে ধন্যবাদ!