הבלוג של ינון פרק

טיפים קצרים וחדשות למתכנתים

[טייפסקריפט] דוגמה טובה לשימוש ב Function Overloading

17/12/2022

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

המשך קריאה

[טייפסקריפט] ואז Parameters הציל את היום

14/12/2022

ספריית התקשורת האהובה עליי בריאקט היא swr. היא יותר קטנה מ react-query ועושה בדיוק מה שאני צריך, אבל יש לה רק בעיה אחת: בשביל להשתמש בה צריך להגדיר לה פונקציית תקשורת. אני כל פעם שוכח מחדש את התחביר וחיפוש בגוגל מביא אותי לאותה תוצאה:

const fetcher = url => fetch(url).then(r => r.json())

אבל איך מתרגמים את זה לטייפסקריפט?

אינטואיציה ראשונה תהיה להגדיר את url בתור string, אבל זה לא מרגיש לגמרי נכון. הפונקציה fetch יכולה לקבל גם אוביקט של פרמטרים, ו swr גם תומך בזה. במלים אחרות מבחינת swr אפשר לכתוב בקומפוננטה:

const { data, error } = useSWR({ url: '...', method: 'POST' });

וזה אמור לעבוד ולשלוח בקשת POST. אם אנחנו מכריחים את ה url להיות רק string אנחנו מבטלים שימוש די סטנדרטי בספריה.

מה שאנחנו באמת רוצים כאן זה להגדיר את fetcher שיקבל בדיוק את הפרמטרים ש fetch מקבלת. ואיך כותבים את זה? תשמחו לשמוע שבטייפסקריפט יש Utility Type שנקרא Parameters. זה עובד ככה:

const fetcher = (...args: Parameters<typeof fetch>) => fetch(...args).then(res => res.json())

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

היום למדתי: סימון פונקציה בתור Deprecated ב TypeScript

08/12/2022

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

עם טייפסקריפט ו VS Code סימון כזה הוא יחסית פשוט:

  1. אנחנו מוסיפים את המילה @deprecated בהערת JSDoc מעל הגדרת הפונקציה אותה רוצים לסמן בתור Deprecated.

  2. באופן אוטומטי כש VS Code ימצא שהשתמשתי באותה פונקציה הוא יסמן קו באמצע של הקוד שמפעיל את הפונקציה (strike through) וכשאני אעבור על הסימון עם העכבר אני אוכל לראות את כל ההערה, שם בדרך כלל תהיה המלצה במה להשתמש במקום.

דוגמה? בטח. נסו להדביק את הקוד הבא בקובץ טייפסקריפט ותראו את הקסם:

/**
 * @deprecated The method should not be used. Use `bar` instead.
 */
function foo(x: number) {
    return x + x;
}

function bar(x: number) {
    return x * 2;
}

const x = foo(10);
const y = bar(20);

ואם אין לכם פרויקט טייפסקריפט זמין אפשר לראות את האפקט גם לייב ב TypeScript Playground בקישור הזה.

טיפ טייפסקריפט: ההבדל בין let ו const מבחינת Type Inference

31/10/2022

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

במילים אחרות ויותר ספציפי, הקוד הזה מגדיר את x להיות מהטיפוס הליטרלי 10:

const x = 10;

אבל הקוד הזה מגדיר את x להיות number:

let x = 10;

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

const movie = { name: 'Return Of The Jedi', rating: 5 };
let key = Math.random() > 0.5 ? "name" : "rating";
console.log(movie[key]);

ולא בגלל שטייפסקריפט כועס על הדירוג הגבוה של "שובו של הג'דיי". הוא פשוט רואה את ה let לפני ה key, מחליט ש key הוא string ולא מבין איך אפשר להשתמש ב string כללי בתור מפתח בגישה לאוביקט movie.

התיקון הכי קל הוא להפוך את הגדרת המשתנה ל const:

const movie = { name: 'Return Of The Jedi', rating: 5 };
const key = Math.random() > 0.5 ? "name" : "rating";
console.log(movie[key]);

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

נ.ב. לאלה מכן שפחות אוהבים להתחכם, אפשר לכתוב את אותה הדוגמה בלי ה random בכלל:

const movie = { name: 'Return Of The Jedi', rating: 5 };
let key = "name";
console.log(movie[key]);

אבל זה עלול להרוס את הכיף.

יתרונות של TypeScript למתכנתי JavaScript

26/10/2022

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

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

המשך קריאה

טיפ טייפסקריפט: מחליפים if ב switch

04/10/2022

קוד הטייפסקריפט הבא לא מתקמפל, למרות שאין בו שום דבר לא בסדר:

type AB = "a"|"b";

function test(x: AB): number {
    if (x === "a") {
        return 1;
    } else if (x === "b") {
        return 2;
    }
}

זה הלינק לפלייגראונד אם אתם צריכים הוכחה: https://www.typescriptlang.org/play?#code/C4TwDgpgBAggQlAvFARAQxQHxQIxQbgChCAzAVwDsBjYASwHsKpgIBnYACgA8AuWOAJR8KZALY4IAJygBvQlAVRaJKNySJk6FANnzF+yRGBlJTAIxF9AXygQANq2jLVXdZrw65+g0ZNMATJaKVoRWQA

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

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

function test(x: AB): number {
    switch(x) {
        case "a": return 1;
        case "b": return 2;
    }
}

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

במשבצת התיקונים הפחות טובים אפשר למצוא את זה:

function test(x: AB): number {
    if (x === "a") {
        return 1;
    } else if (x === "b") {
        return 2;
    } else {
        // never happens
        return 0;
    }
}

שמתקן את הבעיה אבל לא מגן עליי משינוי עתידי ב AB.

וגם את זה שסובל מאותה בעיה:

// @ts-ignore
function test(x: AB): number {
    if (x === "a") {
        return 1;
    } else if (x === "b") {
        return 2;
    }
}

תיקון אחרון לפוסט שעובד הפעם אבל פחות מומלץ במקרה הכללי הוא:

function test(x: AB): number {
    return {
        "a": 1,
        "b": 2,
    }[x];
}

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

טיפ טייפסקריפט: חתימה של פונקציה מתוך Literal Types

24/09/2022

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

type Event =
  | { type: 'login', payload: { username: string }}
  | { type: 'logout'}
  | { type: 'sendMessage', payload: { to: string, text: string }};

function handle(event: Event) {}

ועכשיו שורה כזאת תתקמפל:

handle({ type: 'login', payload: { username: 'yay' }});

אבל שורה כזאת לא תתקמפל:

handle({ type: 'login', payload: { to: 'yay', text: 'abc' }});

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

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

ניסיון ראשון עשוי להיראות כך:

type Event =
  | { type: 'login', payload: { username: string }}
  | { type: 'logout'}
  | { type: 'sendMessage', payload: { to: string, text: string }};

function handle(eventType: Event["type"], eventPayload: Event["payload"]) {}

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

type Event =
  | { type: 'login', payload: { username: string }}
  | { type: 'logout', payload: undefined }
  | { type: 'sendMessage', payload: { to: string, text: string }};

function handle(eventType: Event["type"], eventPayload?: Event["payload"]) {}

אבל אני לא מקבל את מה שרציתי - טייפסקריפט יקמפל את השורה הזאת למרות שבאירוע login כן צריך להעביר שם משתמש:

handle("login");

בעצם מה שהקוד שלי עשה זה ליצור פונקציה שמקבלת בתור פרמטר ראשון משהו שמופיע ב type של Event, ובתור פרמטר שני משהו שמופיע ב payload, בלי להתאים ביניהם.

בשביל ההתאמה בין שני הפרמטרים אני רק צריך להפוך את הפונקציה ל Generic. זה ייתן לי גישה לטיפוסים שבאמת עבר לתוך Event["type"] ואז בעזרתו אני יכול לצמצם את האיחוד:

type Event =
  | { type: 'login', payload: { username: string }}
  | { type: 'logout', payload: undefined }
  | { type: 'sendMessage', payload: { to: string, text: string }};

function handle<E extends Event, EventType extends E["type"]>(
  eventType: EventType,
  eventPayload: Extract<Event, { type: EventType }>["payload"]
) {}

handle("login", { username: "ynon" });
handle("logout", undefined);
handle("sendMessage", { to: "ynon", text: "hi ;)"});

עכשיו שלושת הקריאות מתקמפלות, אבל קריאות שלא מתאימות לחתימה לא יתקמפלו. למשל זה לא יעבור:

handle("login", { to: "me", text: "bye" });

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

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

function handle<E extends Event, EventType extends E["type"]>(
  eventType: EventType,
  eventPayload: Extract<Event, { type: EventType }>["payload"] extends undefined
    ? "MAKE EVENT PAYLOAD OPTIONAL"
    : "USE THE VALUE FROM EXTRACT ..."
) {}

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

type Event =
  | { type: 'login', payload: { username: string }}
  | { type: 'logout', payload: undefined }
  | { type: 'sendMessage', payload: { to: string, text: string }};

function handle<
  E extends Event,
  EventType extends E["type"],
  EventPayload extends Extract<Event, { type: EventType }>["payload"]
>(
  eventType: EventType,
  ...eventPayload: EventPayload extends undefined
      ? [undefined?] 
      : [EventPayload]
) {}

handle("login", { username: "ynon" });
handle("logout");
handle("sendMessage", { to: "ynon", text: "hi ;)"});

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

טיפ טייפסקריפט: זהירות Tuples

17/09/2022

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

const square = [10, 5, 2, 'red'];

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

type User = [number, string, string];

const users: Array<User> = [
    [0, 'a@gmail.com', 'a'],
    [1, 'b@gmail.com', 'b'],
    [2, 'c@gmail.com', 'c'],
];

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

type User = [number, string, string];

const users: Array<User> = [
    [0, 'a@gmail.com', 'a'],
    [1, 'b@gmail.com', 'b'],
    [2, 'c@gmail.com', 'c'],
];

users.push(['d']);

אז אם הכל כל כך טוב למה הכותרת של הפוסט הזה מתחילה במילה "זהירות" אתם שואלים? נו, הבעיה עם Tuples זה שמאוד קל לעבוד על טייפסקריפט. הקוד הזה דווקא כן מתקמפל:

type User = [number, string, string];

const users: Array<User> = [
    [0, 'a@gmail.com', 'a'],
    [1, 'b@gmail.com', 'b'],
    [2, 'c@gmail.com', 'c'],
];

const u: User = [3, 'd@gmail.com', 'd'];
u.splice(0, 2);
// u is now: ['d']
users.push(u);

שווה לשים לב שכשמשתמשים ב Tuples כדאי תמיד להוסיף להם Readonly כדי שמשתמשים לא יוכלו (בכוונה או בטעות) לרמות. ככה זה נראה כשה Tuple לקריאה בלבד:

type User = Readonly<[number, string, string]>;

const users: Array<User> = [
    [0, 'a@gmail.com', 'a'],
    [1, 'b@gmail.com', 'b'],
    [2, 'c@gmail.com', 'c'],
];

const u: User = [3, 'd@gmail.com', 'd'];
u.splice(0, 2);

ועכשיו הקוד שוב לא מתקמפל וטייפסקריפט כותב על השורה האחרונה:

Property 'splice' does not exist on type 'readonly [number, string, string]'. Did you mean 'slice'?