عندما تقوم بنمذجة الكيانات باستخدام TypeScript، من الشائع جدًا الحصول على واجهة مثل هذه:
interface User {
id: number
username: string
}
interface Order {
id: number
userId: number
title: string
year: number
month: number
day: number
amount: { currency: 'EUR' | 'USD', value: number }
}
المشكلة
أنواع الخصائص ليس لها معنى دلالي. من حيث الأنواع، User.id
، Order.id
، Order.year
، إلخ. هي نفسها: رقم، وكأرقام فهي قابلة للتبادل، لكن دلاليًا، ليست كذلك.
استنادًا إلى المثال السابق، يمكن أن يكون لدينا مجموعة من الدوال التي تقوم بعمليات على الكيانات. على سبيل المثال:
function getOrdersFiltered(userId: number, year: number, month: number, day: number, amount: number) { // ...}
function deleteOrder(id: number) { // ... }
ستقبل هذه الدوال أي رقم في أي وسيط بغض النظر عن المعنى الدلالي للرقم. على سبيل المثال:
const id = getUserId()
deleteOrder(id)
من الواضح أن ذلك خطأ كبير، وقد يبدو من السهل تجنب قراءة الشيفرة، لكن الشيفرة ليست دائمًا بسيطة كما في المثال.
نفس الشيء يحدث مع getOrdersFiltered
: يمكننا تبديل قيم اليوم والشهر، ولن نحصل على أي تحذير أو خطأ. ستحدث الأخطاء إذا كان اليوم أكبر من 12، لكن من الواضح أن النتيجة لن تكون كما هو متوقع.
الحل
تقدم قواعد تمارين الكائنات حلاً لذلك: لف جميع البدائيات والسلاسل (نمط مكافحة هوس البدائيات المرتبط). القاعدة هي لف البدائيات في كائن يمثل معنى دلاليًا (يصف DDD ذلك باسم ValueObjects
).
لكن مع TypeScript، لا نحتاج إلى استخدام الفصول أو الكائنات لذلك: يمكننا استخدام نظام الأنواع لضمان عدم استخدام رقم يمثل شيئًا مختلفًا عن السنة بدلاً من السنة.
أنواع العلامات
يستخدم هذا النمط قابلية توسيع الأنواع لإضافة خاصية تضمن المعنى الدلالي:
type Year = number & { __brand: 'year' }
تقوم هذه السطر البسيط بإنشاء نوع جديد يمكن أن يعمل كرقم – ولكنه ليس رقماً، بل هو سنة.
const year = 2012 as Year
function age(year: Year): number { //... }
age(2012) // ❌ IDE will show an error as 2012 is not a Year
age(year) // ✅
تعميم الحل
لتجنب كتابة نوع لكل نوع مميز، يمكننا إنشاء نوع مساعد مثل:
declare const __brand: unique symbol
export type Branded<T, B> = T & { [__brand]: B }
يستخدم رمزاً فريداً كاسم خاصية العلامة لتجنب التعارض مع خصائصك ويحصل على النوع الأصلي والعلامة كمعاملات عامة.
بهذا، يمكننا إعادة هيكلة نماذجنا ودوالنا كما يلي:
type UserId = Branded<number, 'UserId'>
type OrderId = Branded<number, 'OrderId'>
type Year = Branded<number, 'Year'>
type Month = Branded<number, 'Month'>
type Day = Branded<number, 'Day'>
type Amount = Branded<{ currency: 'EUR' | 'USD', value: number}, 'Amount'>
interface User {
id: UserId
username: string
}
interface Order {
id: OrderId
userId: UserId
title: string
year: Year
month: Month
day: Day
amount: Amount
}
function getOrdersFiltered(userId: UserId, year: Year, month: Month, day: Day, amount: Amount) { // ...}
function deleteOrder(id: OrderId) { // ... }
الآن، في هذا المثال، ستظهر IDE خطأً حيث أن id
هو UserId
وdeleteOrder
يتوقع OrderId
.
const id = getUserId()
deleteOrder(id) // ❌ IDE will show an error as id is UserID and deleteOrder expects OrderId
المقايضات
كنوع من المقايضة الصغيرة، ستحتاج إلى استخدام X
كـ Brand
. على سبيل المثال، const year = 2012 as Year
عندما تقوم بإنشاء قيمة جديدة من بدائي، ولكن هذا يعادل new Year(2012)
إذا كنت تستخدم كائنات القيمة. يمكنك توفير دالة تعمل كنوع من “الباني”:
function year(year: number): Year {
return year as Year
}
التحقق من الصحة مع الأنواع المميزة
الأنواع المميزة مفيدة أيضاً لضمان أن البيانات صحيحة حيث يمكنك أن تمتلك أنواعاً محددة للبيانات التي تم التحقق منها، ويمكنك الوثوق بأن المستخدم قد تم التحقق منه فقط من خلال استخدام الأنواع:
type User = { id: UserId, email: Email}
type ValidUser = Readonly<Brand<User, 'ValidUser'>>
function validateUser(user: User): ValidUser {
// Checks if user is in the database
if (!/* logic to check the user is in database */) {
throw new InvalidUser()
}
return user as ValidUser
}
// We can not pass just a User, needs to be a ValidUser
function doSomethingWithAValidUser(user: ValidUser) {
}
Readonly
ليس إلزامياً، ولكن للتأكد من أن رمزك لن يغير البيانات بعد التحقق منها، يُوصى بشدة بذلك.
ملخص
الأنواع المميزة هي حل بسيط يتضمن ما يلي:
- تحسين قراءة الكود: يجعل من الواضح أي قيمة ينبغي استخدامها في كل حجة
- موثوقية: يساعد على تجنب الأخطاء في الشيفرة التي قد يكون من الصعب اكتشافها؛ الآن تساعدنا بيئة تطوير البرامج (و التحقق من الأنواع) على اكتشاف ما إذا كانت القيمة في المكان الصحيح
- تحقق من صحة البيانات: يمكنك استخدام الأنواع المميزة لضمان أن البيانات صالحة.
يمكنك أن تفكر في الأنواع المميزة كنوع من النسخة من ValueObjects
ولكن دون استخدام الفئات — فقط الأنواع والدوال.
استمتع بقوة الأنواع!
Source:
https://dzone.com/articles/branded-types-in-typescript