עברית

גלו טיפוסים ממותגים ב-TypeScript, טכניקה עוצמתית להשגת טיפוסיות נומינלית במערכת טיפוסים סטרוקטורלית. למדו כיצד לשפר בטיחות טיפוסים ובהירות קוד.

טיפוסים ממותגים ב-TypeScript: טיפוסיות נומינלית במערכת סטרוקטורלית

מערכת הטיפוסים הסטרוקטורלית של TypeScript מציעה גמישות, אך לעיתים עלולה להוביל להתנהגות בלתי צפויה. טיפוסים ממותגים (Branded types) מספקים דרך לאכוף טיפוסיות נומינלית, ובכך משפרים את בטיחות הטיפוסים ואת בהירות הקוד. מאמר זה סוקר טיפוסים ממותגים לעומק, ומספק דוגמאות מעשיות ושיטות עבודה מומלצות ליישומם.

הבנת טיפוסיות סטרוקטורלית מול נומינלית

לפני שנצלול לטיפוסים ממותגים, בואו נבהיר את ההבדל בין טיפוסיות סטרוקטורלית לנומינלית.

טיפוסיות סטרוקטורלית (טיפוסיות ברווז - Duck Typing)

במערכת טיפוסים סטרוקטורלית, שני טיפוסים נחשבים תואמים אם יש להם את אותו מבנה (כלומר, אותם מאפיינים עם אותם טיפוסים). TypeScript משתמשת בטיפוסיות סטרוקטורלית. שקלו את הדוגמה הבאה:


interface Point {
  x: number;
  y: number;
}

interface Vector {
  x: number;
  y: number;
}

const point: Point = { x: 10, y: 20 };
const vector: Vector = point; // תקין ב-TypeScript

console.log(vector.x); // פלט: 10

למרות ש-Point ו-Vector הוגדרו כטיפוסים נפרדים, TypeScript מאפשרת השמה של אובייקט Point למשתנה מסוג Vector מכיוון שהם חולקים את אותו מבנה. זה יכול להיות נוח, אך עלול גם להוביל לשגיאות אם יש צורך להבחין בין טיפוסים שונים מבחינה לוגית אך במקרה בעלי אותה צורה. לדוגמה, חשבו על קואורדינטות של קווי רוחב/אורך שעשויות להתאים במקרה לקואורדינטות פיקסלים על המסך.

טיפוסיות נומינלית

במערכת טיפוסים נומינלית, טיפוסים נחשבים תואמים רק אם יש להם את אותו השם. גם אם לשני טיפוסים יש את אותו המבנה, הם יטופלו כנפרדים אם יש להם שמות שונים. שפות כמו Java ו-C# משתמשות בטיפוסיות נומינלית.

הצורך בטיפוסים ממותגים

הטיפוסיות הסטרוקטורלית של TypeScript יכולה להיות בעייתית כאשר יש צורך להבטיח שערך מסוים שייך לטיפוס ספציפי, ללא קשר למבנה שלו. לדוגמה, שקלו ייצוג של מטבעות. ייתכן שיהיו לכם טיפוסים שונים עבור USD ו-EUR, אך שניהם יכולים להיות מיוצגים כמספרים. ללא מנגנון להבחין ביניהם, תוכלו בטעות לבצע פעולות על המטבע הלא נכון.

טיפוסים ממותגים מטפלים בבעיה זו בכך שהם מאפשרים ליצור טיפוסים נפרדים שהם דומים מבחינה מבנית אך מטופלים כשונים על ידי מערכת הטיפוסים. זה משפר את בטיחות הטיפוסים ומונע שגיאות שאחרת היו עלולות לחמוק.

יישום טיפוסים ממותגים ב-TypeScript

טיפוסים ממותגים מיושמים באמצעות טיפוסי חיתוך (intersection types) וסמל (symbol) ייחודי או ליטרל מחרוזת. הרעיון הוא להוסיף "מותג" לטיפוס שמבדיל אותו מטיפוסים אחרים בעלי מבנה זהה.

שימוש בסמלים (מומלץ)

שימוש בסמלים למיתוג הוא בדרך כלל השיטה המועדפת מכיוון שסמלים מובטחים להיות ייחודיים.


const USD = Symbol('USD');
type USD = number & { readonly [USD]: unique symbol };

const EUR = Symbol('EUR');
type EUR = number & { readonly [EUR]: unique symbol };

function createUSD(value: number): USD {
  return value as USD;
}

function createEUR(value: number): EUR {
  return value as EUR;
}

function addUSD(a: USD, b: USD): USD {
  return (a + b) as USD;
}

const usd1 = createUSD(10);
const usd2 = createUSD(20);
const eur1 = createEUR(15);

const totalUSD = addUSD(usd1, usd2);
console.log("Total USD:", totalUSD);

// הסרת ההערה מהשורה הבאה תגרום לשגיאת טיפוס
// const invalidOperation = addUSD(usd1, eur1);

בדוגמה זו, USD ו-EUR הם טיפוסים ממותגים המבוססים על הטיפוס number. ה-unique symbol מבטיח שהטיפוסים הללו נפרדים. הפונקציות createUSD ו-createEUR משמשות ליצירת ערכים מטיפוסים אלו, והפונקציה addUSD מקבלת רק ערכי USD. ניסיון להוסיף ערך EUR לערך USD יגרום לשגיאת טיפוס.

שימוש בליטרלי מחרוזות

ניתן גם להשתמש בליטרלי מחרוזות למיתוג, אם כי גישה זו פחות חזקה משימוש בסמלים מכיוון שליטרלי מחרוזות אינם מובטחים להיות ייחודיים.


type USD = number & { readonly __brand: 'USD' };
type EUR = number & { readonly __brand: 'EUR' };

function createUSD(value: number): USD {
  return value as USD;
}

function createEUR(value: number): EUR {
  return value as EUR;
}

function addUSD(a: USD, b: USD): USD {
  return (a + b) as USD;
}

const usd1 = createUSD(10);
const usd2 = createUSD(20);
const eur1 = createEUR(15);

const totalUSD = addUSD(usd1, usd2);
console.log("Total USD:", totalUSD);

// הסרת ההערה מהשורה הבאה תגרום לשגיאת טיפוס
// const invalidOperation = addUSD(usd1, eur1);

דוגמה זו משיגה את אותה תוצאה כמו הקודמת, אך משתמשת בליטרלי מחרוזות במקום סמלים. למרות שזה פשוט יותר, חשוב להבטיח שליטרלי המחרוזות המשמשים למיתוג יהיו ייחודיים בבסיס הקוד שלכם.

דוגמאות מעשיות ומקרי שימוש

ניתן ליישם טיפוסים ממותגים במגוון תרחישים בהם יש צורך לאכוף בטיחות טיפוסים מעבר לתאימות מבנית.

מזהים (IDs)

שקלו מערכת עם סוגים שונים של מזהים, כגון UserID, ProductID ו-OrderID. כל המזהים הללו עשויים להיות מיוצגים כמספרים או מחרוזות, אך תרצו למנוע ערבוב מקרי של סוגי מזהים שונים.


const UserIDBrand = Symbol('UserID');
type UserID = string & { readonly [UserIDBrand]: unique symbol };

const ProductIDBrand = Symbol('ProductID');
type ProductID = string & { readonly [ProductIDBrand]: unique symbol };

function getUser(id: UserID): { name: string } {
  // ... שליפת נתוני משתמש
  return { name: "Alice" };
}

function getProduct(id: ProductID): { name: string, price: number } {
  // ... שליפת נתוני מוצר
  return { name: "Example Product", price: 25 };
}

function createUserID(id: string): UserID {
  return id as UserID;
}

function createProductID(id: string): ProductID {
  return id as ProductID;
}

const userID = createUserID('user123');
const productID = createProductID('product456');

const user = getUser(userID);
const product = getProduct(productID);

console.log("User:", user);
console.log("Product:", product);

// הסרת ההערה מהשורה הבאה תגרום לשגיאת טיפוס
// const invalidCall = getUser(productID);

דוגמה זו מדגימה כיצד טיפוסים ממותגים יכולים למנוע העברת ProductID לפונקציה המצפה ל-UserID, ובכך לשפר את בטיחות הטיפוסים.

ערכים ספציפיים לדומיין

טיפוסים ממותגים יכולים להיות שימושיים גם לייצוג ערכים ספציפיים לדומיין עם אילוצים. לדוגמה, ייתכן שיהיה לכם טיפוס עבור אחוזים שתמיד צריך להיות בין 0 ל-100.


const PercentageBrand = Symbol('Percentage');
type Percentage = number & { readonly [PercentageBrand]: unique symbol };

function createPercentage(value: number): Percentage {
  if (value < 0 || value > 100) {
    throw new Error('אחוז חייב להיות בין 0 ל-100');
  }
  return value as Percentage;
}

function applyDiscount(price: number, discount: Percentage): number {
  return price * (1 - discount / 100);
}

try {
  const discount = createPercentage(20);
  const discountedPrice = applyDiscount(100, discount);
  console.log("Discounted Price:", discountedPrice);

  // הסרת ההערה מהשורה הבאה תגרום לשגיאה בזמן ריצה
  // const invalidPercentage = createPercentage(120);
} catch (error) {
  console.error(error);
}

דוגמה זו מראה כיצד לאכוף אילוץ על ערך של טיפוס ממותג בזמן ריצה. בעוד שמערכת הטיפוסים אינה יכולה להבטיח שערך Percentage יהיה תמיד בין 0 ל-100, הפונקציה createPercentage יכולה לאכוף אילוץ זה בזמן ריצה. ניתן גם להשתמש בספריות כמו io-ts כדי לאכוף אימות בזמן ריצה של טיפוסים ממותגים.

ייצוגי תאריך ושעה

עבודה עם תאריכים ושעות יכולה להיות מורכבת בשל פורמטים שונים ואזורי זמן. טיפוסים ממותגים יכולים לעזור להבדיל בין ייצוגי תאריך ושעה שונים.


const UTCDateBrand = Symbol('UTCDate');
type UTCDate = string & { readonly [UTCDateBrand]: unique symbol };

const LocalDateBrand = Symbol('LocalDate');
type LocalDate = string & { readonly [LocalDateBrand]: unique symbol };

function createUTCDate(dateString: string): UTCDate {
  // ודא שמחרוזת התאריך היא בפורמט UTC (למשל, ISO 8601 עם Z)
  if (!/\d{4}-\d{2}-\d{2}T\d{2}:\d{2}:\d{2}.\d{3}Z/.test(dateString)) {
    throw new Error('פורמט תאריך UTC לא תקין');
  }
  return dateString as UTCDate;
}

function createLocalDate(dateString: string): LocalDate {
  // ודא שמחרוזת התאריך היא בפורמט תאריך מקומי (למשל, YYYY-MM-DD)
  if (!/\d{4}-\d{2}-\d{2}/.test(dateString)) {
    throw new Error('פורמט תאריך מקומי לא תקין');
  }
  return dateString as LocalDate;
}

function convertUTCDateToLocalDate(utcDate: UTCDate): LocalDate {
  // בצע המרת אזור זמן
  const date = new Date(utcDate);
  const localDateString = date.toLocaleDateString();
  return createLocalDate(localDateString);
}

try {
  const utcDate = createUTCDate('2024-01-20T10:00:00.000Z');
  const localDate = convertUTCDateToLocalDate(utcDate);
  console.log("UTC Date:", utcDate);
  console.log("Local Date:", localDate);
} catch (error) {
  console.error(error);
}

דוגמה זו מבחינה בין תאריכי UTC לתאריכים מקומיים, ומבטיחה שאתם עובדים עם ייצוג התאריך והשעה הנכון בחלקים שונים של היישום שלכם. אימות בזמן ריצה מבטיח שרק מחרוזות תאריך בפורמט נכון יכולות להיות מוקצות לטיפוסים אלו.

שיטות עבודה מומלצות לשימוש בטיפוסים ממותגים

כדי להשתמש בטיפוסים ממותגים ב-TypeScript ביעילות, שקלו את שיטות העבודה המומלצות הבאות:

יתרונות של טיפוסים ממותגים

חסרונות של טיפוסים ממותגים

חלופות לטיפוסים ממותגים

בעוד שטיפוסים ממותגים הם טכניקה עוצמתית להשגת טיפוסיות נומינלית ב-TypeScript, ישנן גישות חלופיות שכדאי לשקול.

טיפוסים אטומים (Opaque Types)

טיפוסים אטומים דומים לטיפוסים ממותגים אך מספקים דרך מפורשת יותר להסתיר את הטיפוס הבסיסי. ל-TypeScript אין תמיכה מובנית בטיפוסים אטומים, אך ניתן לדמות אותם באמצעות מודולים וסמלים פרטיים.

מחלקות (Classes)

שימוש במחלקות יכול לספק גישה מונחית עצמים יותר להגדרת טיפוסים נפרדים. בעוד שמחלקות הן בעלות טיפוסיות סטרוקטורלית ב-TypeScript, הן מציעות הפרדת עניינים ברורה יותר וניתן להשתמש בהן לאכיפת אילוצים באמצעות מתודות.

ספריות כמו `io-ts` או `zod`

ספריות אלו מספקות אימות טיפוסים מתוחכם בזמן ריצה וניתן לשלב אותן עם טיפוסים ממותגים כדי להבטיח בטיחות הן בזמן קומפילציה והן בזמן ריצה.

סיכום

טיפוסים ממותגים ב-TypeScript הם כלי רב ערך לשיפור בטיחות הטיפוסים ובהירות הקוד במערכת טיפוסים סטרוקטורלית. על ידי הוספת "מותג" לטיפוס, ניתן לאכוף טיפוסיות נומינלית ולמנוע ערבוב מקרי של טיפוסים דומים מבחינה מבנית אך שונים מבחינה לוגית. בעוד שטיפוסים ממותגים מוסיפים מורכבות ותקורה מסוימת, היתרונות של בטיחות טיפוסים ותחזוקתיות קוד משופרת עולים לעיתים קרובות על החסרונות. שקלו להשתמש בטיפוסים ממותגים בתרחישים שבהם עליכם להבטיח שערך מסוים שייך לטיפוס ספציפי, ללא קשר למבנה שלו.

על ידי הבנת העקרונות מאחורי טיפוסיות סטרוקטורלית ונומינלית, ועל ידי יישום שיטות העבודה המומלצות המתוארות במאמר זה, תוכלו למנף ביעילות טיפוסים ממותגים כדי לכתוב קוד TypeScript חזק וניתן לתחזוקה. החל מייצוג מטבעות ומזהים ועד לאכיפת אילוצים ספציפיים לדומיין, טיפוסים ממותגים מספקים מנגנון גמיש ועוצמתי לשיפור בטיחות הטיפוסים בפרויקטים שלכם.

בזמן שאתם עובדים עם TypeScript, חקרו את הטכניקות והספריות השונות הזמינות לאימות ואכיפת טיפוסים. שקלו להשתמש בטיפוסים ממותגים בשילוב עם ספריות אימות בזמן ריצה כמו io-ts או zod כדי להשיג גישה מקיפה לבטיחות טיפוסים.