טיפוס פונקציה מתאר את הפרמטרים ואת ערך ההחזרה של פונקציה, ונכתב עם חץ: (a: number, b: number) => number. תנו לו שם עם type, ותוכלו להשתמש בו למשתנים, לפרמטרים ולמאפיינים של אובייקטים.
הפונקציה שמושמת ל-add לא צריכה הערות טיפוס: טיפוסי הפרמטרים שלה מגיעים מ-Operation. זה נקרא contextual typing, וזו הסיבה העיקרית לתת שמות לטיפוסי פונקציות.
התחביר של טיפוס פונקציה
הצורה היא (parameters) => ReturnType. כל פרמטר צריך שם וטיפוס. השמות נמצאים שם בשביל הקריאות והרמזים בעורך; כל פונקציה עם טיפוסי פרמטרים תואמים מתאימה, לא משנה איך היא קוראת להם.
type Predicate = (value: number) => boolean;
type Formatter = (value: number, digits?: number) => string; // optional parameter
type Logger = (...parts: string[]) => void; // rest parameter
type Factory = () => { id: number }; // no parameters
השם אינו אופציונלי. (string) => void מצהיר על פרמטר בשם string בלי טיפוס, ומצב strict דוחה אותו עם TS7051: Parameter has a name but no type. Did you mean 'arg0: string'?. כתבו (value: string) => void.
טיפוסים לפונקציות חץ
אפשר לתת טיפוס לפונקציית חץ בשתי דרכים. להוסיף הערות טיפוס לפונקציה עצמה, או לתת למשתנה טיפוס פונקציה ולתת לפרמטרים להיות מוסקים.
צורה 1 היא הבחירה הרגילה לפונקציה עצמאית. צורה 3 משתלמת כשכמה פונקציות חולקות חתימה אחת (handlers, פונקציות השוואה, פונקציות המרה), כי החתימה נכתבת פעם אחת ושינוי בה נבדק בכל מקום.
טיפוסי callback
פרמטר שהטיפוס שלו הוא טיפוס פונקציה הוא callback. הפונקציה שמעבירים נבדקת מולו, והפרמטרים שלה מוסקים ממנו.
שני כללים הופכים callbacks לנוחים לשימוש:
- פחות פרמטרים זה בסדר. אפשר להעביר פונקציה שמקבלת פרמטר אחד במקום שבו מוצעים שניים. JavaScript מתעלם מארגומנטים עודפים, ולכן TypeScript מאפשר את זה. יותר פרמטרים ממה שהטיפוס מציע זו שגיאה (
Target signature provides too few arguments.). - החזרה מסוג
voidמקבלת כל דבר. callback מהטיפוס(...) => voidיכול להחזיר ערך; מי שקורא לו מתחייב לא להשתמש בו. זו הסיבה ש-list.forEach((x) => other.push(x))מתקמפל, למרות ש-pushמחזיר מספר.
טיפוסי הפרמטרים עדיין חייבים להיות תואמים. העברה של (x: string) => ... במקום שבו מצופה (a: number, b: number) => number נכשלת עם Types of parameters 'x' and 'a' are incompatible.
חתימות קריאה
(n: number) => string הוא קיצור לטיפוס אובייקט עם חתימת קריאה: { (n: number): string }. צריך את הצורה הארוכה כשלפונקציה יש גם מאפיינים.
TypeScript מאפשר להוסיף מאפיינים לפונקציה שהוצהרה עם const באותו scope, ועוקב אחריהם בטיפוס של הפונקציה. גם interface יכול להכיל חתימת קריאה: interface Counter { (): number; count: number }.
חתימת בנייה (construct signature) מתארת משהו שקוראים לו עם new. היא מוסיפה new בהתחלה: new (name: string) => User, או { new (name: string): User } בצורת אובייקט. כך נותנים טיפוס לפרמטר שמקבל מחלקה.
תחביר מתודה מול תחביר מאפיין
בטיפוס אובייקט, אפשר לכתוב איבר שהוא פונקציה כמתודה, handle(value: string): void, או כמאפיין שמחזיק פונקציה, handle: (value: string) => void. הם נראים זהים, אבל תחת strict הם נבדקים אחרת:
הקומפיילר מדווח:
index.ts(8,27): error TS2322: Type '(value: string) => void' is not assignable to type '(value: string | number) => void'.
Types of parameters 'value' and 'value' are incompatible.
Type 'string | number' is not assignable to type 'string'.
Type 'number' is not assignable to type 'string'.
השגיאה צודקת: onlyStrings לא יודעת לטפל במספר. צורת המתודה מעבירה את אותה טעות (פרמטרים של מתודות נבדקים "bivariantly" מסיבות היסטוריות), ו-a.handle(42) הייתה קורסת בזמן ריצה עם TypeError: value.toUpperCase is not a function. מחקו את השורה של b כדי לראות את הקריסה. כשאתם כותבים טיפוסי אובייקט משלכם, צורת המאפיין תופסת יותר טעויות.
הימנעו מהטיפוס Function
Function הוא הטיפוס המובנה שכל ערך פונקציה עומד בו. כמעט אף פעם זה לא מה שאתם רוצים: TypeScript לא מכיר את הפרמטרים או את טיפוס ההחזרה, ולכן כל קריאה מתקבלת ומחזירה any.
החליפו את Function בחתימה האמיתית. כשבאמת מקבלים כל פונקציה (debounce גנרי, עטיפה ללוגים), (...args: never[]) => unknown מקבל כל פונקציה ועדיין שומר את התוצאה כ-unknown במקום any. לפונקציות שהטיפוסים שלהן תלויים בקלט, ראו generics: טיפוס של פונקציה גנרית נראה כמו <T>(value: T) => T.
שאלות נפוצות
איך מגדירים טיפוס של פונקציה ב-TypeScript?
משתמשים בתחביר החץ: (a: number, b: number) => number. נותנים לו שם עם type alias, type Compare = (a: number, b: number) => number;, ומשתמשים בשם הזה למשתנים, לפרמטרים ולמאפיינים. שמות הפרמטרים הם חלק מהתחביר אבל משמשים רק לתיעוד: פונקציה עם שמות פרמטרים אחרים עדיין מתאימה.
איך מגדירים טיפוס לפרמטר callback ב-TypeScript?
כותבים את טיפוס הפונקציה בתור הטיפוס של הפרמטר: function onEach(items: string[], cb: (item: string, index: number) => void) { ... }. מי שקורא לפונקציה יכול להעביר פונקציה שמקבלת פחות פרמטרים, והפרמטרים של ה-callback עצמו מוסקים מהטיפוס הזה, כך ש-onEach(list, (item) => ...) לא צריך הערת טיפוס.
למה לא כדאי להשתמש בטיפוס Function ב-TypeScript?
Function מקבל כל פונקציה, והקריאה אליו לא נבדקת: מותר להעביר כל ארגומנט והתוצאה היא any. כתבו במקום זה את החתימה האמיתית, כמו () => void או (value: string) => number, או (...args: never[]) => unknown כשבאמת מקבלים כל פונקציה.
מה ההבדל בין טיפוס פונקציה לחתימת קריאה?
הם מתארים את אותו הדבר. (n: number) => string הוא קיצור לטיפוס האובייקט { (n: number): string }. צריך את צורת חתימת הקריאה כשלפונקציה יש גם מאפיינים, למשל { (n: number): string; label: string }.
איך מגדירים טיפוס לפונקציית חץ ב-TypeScript?
או שמוסיפים הערות טיפוס לפרמטרים ולטיפוס ההחזרה בתוך הפונקציה, const half = (n: number): number => n / 2;, או שנותנים למשתנה טיפוס פונקציה ונותנים לפרמטרים להיות מוסקים: const half: (n: number) => number = (n) => n / 2;.