בפוסט הזה אולי לא נתקדם הלאה כי אני רוצה להבהיר כמה נקודות ולחזור על חלק מהמושגים מהפוסט הקודם
הודעות
א. תודה לכל מי שטרח להשאיר לי משוב, אם בצורת הצבעה על הפוסט ואם בערוצים פרטיים (כמה חברים כתבו אלי בערוצים אחרים), אני מעריך את זה!
ב. לכל מי שהתקשה להבין את הפוסט הקודם:
אל תאשים את עצמך. זה לא היה פוסט קל להבנה! כתבתי דברים בקיצור, ולפעמים השמטתי לגמרי הסברים נחוצים.
הנושא עצמו מאוד תיאורטי, וגם הדוגמאות לא היו ממש מעשיות.
אני מתנצל. (אנסה להבא להביא דוגמאות יותר מעשיות)
ולמי שלא התקשה כלל: או שיש לך הכירות קודמת עם הנושא, או שאתה מוכשר, או שלא הבנת באמת
ג. חשוב להבהיר! אין שום צורך להכיר את כל התוכן התיאורטי שכתבתי כדי לקבל תועלת מ-TS. גם אם הנושא לא מושך אותך או אפילו דוחה אותך, אני עדיין ממליץ בחום להשתמש ב-TS כי אפשר לקבל רוב התועלת בלי להבין את הרקע התיאורטי. הפוסט שלי חשוב למי שרוצה ליצור טייפים. רוב הזמן אתה צורך טייפים שאחרים כתבו ולא יוצר טייפים חדשים. וגם רוב הטייפים שתיצור יהיו פשוטים בלי צורך בידע תיאורטי עמוק.
ד. העירו לי על כמה דברים שלא הסברתי די הצורך בפוסט הקודם ואנסה להבהיר אותם כאן
מה זה טייפ?
(הרחבה של הקטע "מה זה טייפ" בפוסט הקודם)
התפקיד של טייפים בשפות תכנות זהה לכל השפות: בהנתן ערך כלשהו, המתכנת רוצה לדעת בצורה סטטית (כלומר, בלי להריץ את התוכנה בפועל) איזה פעולות זמינים לו על ערך זה, איך מותר להשתמש בערך.
אבל יש הבדל מהותי בין ההתעסקות עם טייפים בשפות סטטיות (statically typed) "קלאסיות" (JAVA, C++, C#, etc) לטייפים ב-TS
כי בשפות קלאסיות אין עיסוק ישיר עם טייפים עצמם. עוסקים עם קלאסים, שהם "תבניות" או "אב טיפוס" לערכים, קלאס מגדיר איזה שדות הערך חייב להכיל, ובאיזה פעולות הוא צריך לתמוך. וכתוצאה נוצר "טייפ" (שהוא הסט של כל הערכים האפשריים שמתאימים לתבנית) אבל אין עיסוק ישיר עם טייפים עצמם
ב-TS יש עיסוק ישיר עם טייפים עצמם. מחברים טייפים אחד לשני, מחסרים טייפים אחד מהשני, בונים טייפים מטייפים אחרים, דברים שלא קיימים בשפות קלאסיות.
בשפה קלאסית אין מושג של literal type (הטייפ של ערך literal, סט בעל ערך יחיד). כי כל טייפ הוא תוצאה מהצהרה של קלאס שהוא תבנית להרבה ערכים כי העיקר של הטייפ הוא הפעולות שזמינים עליו.
לא כן ב-TS שאפשר לעסוק ישירות עם המושג טייפ.
לכן חשוב לחשוב על טייפים ב-TS כסטים, במקום כתבניות כמו בשפות קלאסיות
GENERICS
הדוגמה שהבאתי בפוסט הקודם לא היה מספיק ברור וגם לא כל כך מעשי
אביא דוגמה מעשי ביותר
נגיד שאתה בונה קליינט ל-API
אתה יודע שה-API המדובר תמיד מחזיר תשובות בתבנית זה:
type ApiResponse = {
success: boolean
data: object | undefined
error: string | undefined
}
השימוש ככה:
type User = { ... }
type Transaction = { ... }
class ApiClient {
// users
getUserById(id: number): ApiResponse { ... }
updateUser(newUser: User): ApiResponse { ... }
// transactions
listTransactions(userId: number): ApiResponse { ... }
// ...
}
const apiClient = new ApiClient
const res: ApiResponse = apiClient.getUserById(id)
if (res.success) {
const user = res.data
// do something with user
} else {
console.error(error)
}
הבעיה בקוד הזה הוא שבשורה 18, הדבר היחיד ש-TS יודע על המשתנה user הוא שזה מסוג object אבל הוא לא יודע איזה סוג אובייקט.
דרך אחת לטפל בזה הוא להצהיר על טייפ בשם UserApiResponse ושם להצהיר על הטייפ המלא של שדה data
הבעיה היא שצריך לחזור שוב ושוב על הצהרה זו לכל סוג של XXXApiResponse
אפשרות אחרת הוא להצהיר שהטייפ של res.data הוא User
const user = res.data as User
אבל בצורה זו מאבדים את הבטחת האבטחה של TS וצריך לסמוך על המתכנת שהצהיר נכון על הטייפ
הפתרון הוא להשתמש ב-generic שהוא "טייפ בונה טייפים"
ככה:
type ApiResponse<T> = {
success: boolean
data: T | undefined
error: string | undefined
}
type User = { ... }
type Transaction = { ... }
class ApiClient {
// users
getUserById(id: number): ApiResponse<User> { ... }
updateUser(newUser: User): ApiResponse<User> { ... }
// transactions
listTransactions(userId: number): <Transaction[]>ApiResponse { ... }
// ...
}
const apiClient = new ApiClient
const res: ApiResponse = apiClient.getUserById(id)
if (res.success) {
const user = res.data
// user is type User
} else {
console.error(error)
}
בפסאודו קוד ההצהרה דומה לקוד כזה:
const ApiResponse = function(T: type): type {
return new Type(`{
success: boolean
data: ${T} | undefined
error: string | undefined
}`)
}
תנאים
יש כמה בעיות בדוגמה שהבאתי למעלה עבור שימוש בתנאי
א. זה לא כל כך מעשי
ב. יש באג (הטייפ לא עושה מה שהתכוונתי שהוא יעשה - אולי אכתוב על זה בפוסט אחר) ואין לי ממש דרך לפתור את הבאג
ג. השתמשתי בתחביר שלא הצגתי מקודם (אינדוקס)
אני רוצה להביא דוגמה מעשית פשוטה שבונה על הדוגמה הקודמת
נגיד שה-API מחזיר לפעמים אובייקט יחיד ולפעמים מערך
אם הוא מחזיר מערך אז יש שדות נוספות בתשובה
אולי נגיד שדה count שמחזיר כמה אובייקטים יש בסה"כ
איך נגדיר type כזה?
type BaseApiResponse<T> = {
success: boolean
data: T | undefined
error: string | undefined
}
type ApiResponse<T> = T extends any[] ? BaseApiResponse<T> & { count: number } : BaseApiResponse<T>
אפשר לתרגם את זה לקוד פסאודו זה:
function ApiResponse(T: type): type {
const base = `{
success: boolean
data: ${T} | undefined
error: string | undefined
}`
if (T instanceof Array(any)) {
return `base & { count: number }`
} else {
return base
}
}
Mapped Types
הבאנו למעלה דוגמה שהיא גם מסובכת מדי וגם לא כל כך מעשי לשימוש יומיומי
אני רוצה להביא דוגמה נורמלית מתוך הטייפים המובנים (TS מגיע עם "ספרייה סטנדרטית" של טייפים מובנים)
נבנה על הדוגמה הקודמת שלנו של קליינט API
אז אתה רוצה להגדיר שהפונקציה updateUser מקבל אובייקט מסוג User אבל בשונה מ-User, כל השדות הם אופציונליות. (כי מי שרוצה לעדכן רק שדה אחת לא צריך לשלוח את כל השדות)
אז אפשר לכתוב טייפ חדש
type User = {
name: string
foo: string
bar: number
}
type UserUpdate = {
name?: string
foo?: string
bar?: number
}
אבל זה בזבוז נייר דיו וביטים, וכמובן זה לא DRY מה שאמור לזעזע כל מתכנת ששוה משהו
הפתרון הוא:
updateUser(newUser: Partial<User>): ApiResponse<User> { ... }
השתמשנו ב-helper (כך נקראים "פקונציות" אלו שעוזרים לבניית טייפים מבוססים טייפים קיימים) בשם Partial שלוקח טייפ ומחזיר טייפ עם אותם שדות, רק שכולם נהיים אופציונליים
איך עובד ה-helper הזה?
נסתכל בקוד המקור:
type Partial<T> = { [P in keyof T]?: T[P] | undefined; }
מילת המפתח in עובד ככה: עבור כל חבר ב-union שצד הימני מייצג, הוא מריץ את הביטוי שאחרי הנקדותיים כאשר המשתנה P מייצג את החבר הזה מה-union
שימו לב לתחביר שלא הצגנו מקודם: T[P] - זה אינדוקס בטייפ שעובד בדיוק כמו אינדוקס אובייקט רגיל. T[P] הוא הטייפ של שדה P בתוך טייפ T
נתרגם את זה לפסאודו קוד:
function keyOf(o: object): union {
return new Union(Object.keys(o))
}
function Partial(T: type): type {
const res = {}
for (const P in keyOf(T)) {
res[P] = T[P] | undefined
}
return res
}
(סליחה. בפסאודו קוד הזה השתמשתי במוסכמות אחרות מהקטעים הקודמים. בקודמים יצרתי טייפ על ידי העברת מחרוזת ל"בנאי של הטייפ" - כביכול. הפעם לשם נוחות התייחסתי לטייפ כאובייקט. זה רק פסאודו קוד כמובן ואני מקווה שהכוונה ברורה)