5.4 פונקציות וצמצום טיפוסים הרצאה
פונקציות וצמצום טיפוסים - functions and narrowing¶
בשיעור הזה נלמד שני נושאים מרכזיים: איך טייפסקריפט מטפלת בפונקציות, ואיך היא מצמצמת טיפוסים כדי לעבוד עם union types בצורה בטוחה.
פונקציות מוטייפות - typed functions¶
כבר ראינו שפרמטרים של פונקציות צריכים טיפוסים. בואו נעמיק:
// basic typed function
function add(a: number, b: number): number {
return a + b;
}
// arrow function with types
let multiply = (a: number, b: number): number => a * b;
// return type is usually inferred - no need to write it
function greet(name: string) {
return `Hello, ${name}!`; // TS infers return type: string
}
כלל: טיפוס ההחזרה בדרך כלל לא חייב - TS מסיקה אותו. אבל בפונקציות מורכבות (או כשרוצים תיעוד ברור) כדאי לכתוב אותו.
פרמטרים אופציונליים ועם ברירת מחדל¶
// optional parameter - must come after required ones
function createUser(name: string, age?: number): string {
if (age !== undefined) {
return `${name}, age ${age}`;
}
return name;
}
createUser("Alice"); // "Alice"
createUser("Alice", 30); // "Alice, age 30"
// default value - also makes the parameter optional
function createUser2(name: string, role: string = "user"): string {
return `${name} (${role})`;
}
createUser2("Alice"); // "Alice (user)"
createUser2("Alice", "admin"); // "Alice (admin)"
פרמטרים מסוג rest¶
function sum(...numbers: number[]): number {
return numbers.reduce((total, n) => total + n, 0);
}
sum(1, 2, 3); // 6
sum(1, 2, 3, 4, 5); // 15
טיפוס של פונקציה - function type¶
אפשר להגדיר את הטיפוס של פונקציה בנפרד:
type Formatter = (value: number) => string;
let toFixed: Formatter = (value) => value.toFixed(2);
let toCurrency: Formatter = (value) => `$${value.toFixed(2)}`;
function formatValues(values: number[], formatter: Formatter): string[] {
return values.map(formatter);
}
formatValues([1.5, 2.333, 10], toFixed); // ["1.50", "2.33", "10.00"]
formatValues([1.5, 2.333, 10], toCurrency); // ["$1.50", "$2.33", "$10.00"]
עומס - function overloads¶
לפעמים פונקציה מתנהגת אחרת בהתאם לטיפוס הקלט. overloads מאפשרים להגדיר מספר חתימות:
// overload signatures
function parse(value: string): number;
function parse(value: number): string;
// implementation signature - must handle all overloads
function parse(value: string | number): number | string {
if (typeof value === "string") {
return parseInt(value);
}
return value.toString();
}
let num = parse("42"); // TS knows: number
let str = parse(42); // TS knows: string
החתימות העליונות הן מה שהמשתמש רואה. חתימת ה-implementation היא פנימית ולא נראית מבחוץ.
דוגמה מעשית - פונקציה שמחזירה אלמנט או מערך:
function getElement(id: string): HTMLElement;
function getElement(className: string, all: true): HTMLElement[];
function getElement(selector: string, all?: boolean): HTMLElement | HTMLElement[] {
if (all) {
return Array.from(document.querySelectorAll(selector)) as HTMLElement[];
}
return document.querySelector(selector) as HTMLElement;
}
let one = getElement("#header"); // HTMLElement
let many = getElement(".item", true); // HTMLElement[]
הערה: overloads הם כלי חזק אבל מסובך. הרבה פעמים אפשר להשיג את אותה תוצאה עם union types פשוטים.
אישור טיפוס - type assertions¶
לפעמים אנחנו יודעים יותר מ-TS על הטיפוס. as מאפשר "להגיד" ל-TS מה הטיפוס:
// TS doesn't know what querySelector returns specifically
let input = document.querySelector("#username") as HTMLInputElement;
input.value = "Alice"; // ok - TS trusts us that it's an HTMLInputElement
// alternative syntax (doesn't work in JSX/TSX files)
let input2 = <HTMLInputElement>document.querySelector("#username");
זהירות: type assertions לא מבצעות בדיקה ב-runtime. אם טעיתם, הקוד יקרוס:
let value: unknown = "hello";
let num = value as number; // no error at compile time
console.log(num.toFixed(2)); // CRASH at runtime! "hello" doesn't have toFixed
כלל: השתמשו ב-type assertions רק כשאתם בטוחים. אם אפשר, העדיפו narrowing (ראו בהמשך).
צמצום טיפוסים - type narrowing¶
צמצום הוא הדרך שטייפסקריפט "מצמצמת" טיפוס רחב לטיפוס ספציפי יותר בתוך בלוק קוד. זו אחת התכונות הכי חזקות של השפה.
typeof¶
הבדיקה הכי בסיסית - typeof מצמצמת טיפוסים פרימיטיביים:
function printValue(value: string | number): void {
if (typeof value === "string") {
// here TS knows value is string
console.log(value.toUpperCase());
} else {
// here TS knows value is number
console.log(value.toFixed(2));
}
}
typeof עובד עם: "string", "number", "boolean", "undefined", "object", "function", "bigint", "symbol".
instanceof¶
בודק אם אובייקט הוא מופע של class:
function formatError(error: Error | string): string {
if (error instanceof Error) {
// here TS knows error is Error
return `Error: ${error.message}\nStack: ${error.stack}`;
}
// here TS knows error is string
return `Error: ${error}`;
}
class Dog {
bark() { console.log("Woof!"); }
}
class Cat {
meow() { console.log("Meow!"); }
}
function makeSound(animal: Dog | Cat): void {
if (animal instanceof Dog) {
animal.bark(); // ok - TS knows it's Dog
} else {
animal.meow(); // ok - TS knows it's Cat
}
}
אופרטור in¶
בודק אם property קיים באובייקט:
interface Fish {
swim(): void;
}
interface Bird {
fly(): void;
}
function move(animal: Fish | Bird): void {
if ("swim" in animal) {
animal.swim(); // ok - TS knows it has swim
} else {
animal.fly(); // ok - TS knows it has fly
}
}
in שימושי כשאין class (ולכן אי אפשר להשתמש ב-instanceof) ורוצים להבדיל בין אינטרפייסים.
איחוד מובחן - discriminated unions¶
זו הדרך הכי נפוצה והכי נקייה לצמצם טיפוסים. כל טיפוס ב-union מקבל שדה type (או כל שם אחר) עם ערך ליטרלי ייחודי:
interface Circle {
type: "circle";
radius: number;
}
interface Rectangle {
type: "rectangle";
width: number;
height: number;
}
interface Triangle {
type: "triangle";
base: number;
height: number;
}
type Shape = Circle | Rectangle | Triangle;
function getArea(shape: Shape): number {
switch (shape.type) {
case "circle":
// TS knows shape is Circle
return Math.PI * shape.radius ** 2;
case "rectangle":
// TS knows shape is Rectangle
return shape.width * shape.height;
case "triangle":
// TS knows shape is Triangle
return (shape.base * shape.height) / 2;
}
}
ה-discriminator (במקרה הזה type) הוא השדה שמבדיל בין הטיפוסים. כשבודקים את ערכו, TS יודעת בדיוק באיזה טיפוס מדובר.
פרדיקט טיפוסים - type predicates¶
פונקציה שמחזירה value is Type אומרת ל-TS שאם הפונקציה מחזירה true, הערך הוא מהטיפוס הזה:
interface Fish {
swim(): void;
}
interface Bird {
fly(): void;
}
// type predicate - the return type is 'animal is Fish'
function isFish(animal: Fish | Bird): animal is Fish {
return "swim" in animal;
}
function move(animal: Fish | Bird): void {
if (isFish(animal)) {
animal.swim(); // TS knows it's Fish
} else {
animal.fly(); // TS knows it's Bird
}
}
type predicates שימושיים כשלוגיקת הבדיקה מורכבת ורוצים לחלץ אותה לפונקציה נפרדת.
דוגמה מעשית - סינון null מתוך מערך:
function isNotNull<T>(value: T | null): value is T {
return value !== null;
}
let values: (string | null)[] = ["hello", null, "world", null, "!"];
let filtered = values.filter(isNotNull);
// filtered: string[] - no more nulls!
בדיקה ממצה עם never - exhaustive checking¶
never מאפשר לוודא שטיפלנו בכל המקרים האפשריים:
type Shape = Circle | Rectangle | Triangle;
function getArea(shape: Shape): number {
switch (shape.type) {
case "circle":
return Math.PI * shape.radius ** 2;
case "rectangle":
return shape.width * shape.height;
case "triangle":
return (shape.base * shape.height) / 2;
default:
// if we handled all cases, shape is 'never' here
const exhaustiveCheck: never = shape;
return exhaustiveCheck;
}
}
למה זה שימושי? אם מוסיפים טיפוס חדש ל-union (למשל Pentagon) ושוכחים לטפל בו, TS תתן שגיאת קומפילציה ב-default case - כי Pentagon לא ניתן להשמה ל-never.
// later, someone adds Pentagon to the union:
type Shape = Circle | Rectangle | Triangle | Pentagon;
// now getArea gives a compile error:
// Type 'Pentagon' is not assignable to type 'never'
זה מבטיח שלעולם לא נשכח לטפל במקרה חדש.
שרשור צמצום - control flow narrowing¶
TS עוקבת אחרי הטיפוס לאורך כל הפונקציה:
function processValue(value: string | number | null): string {
if (value === null) {
return "null";
}
// here value is string | number (null was eliminated)
if (typeof value === "number") {
return value.toFixed(2);
}
// here value is string (number was eliminated too)
return value.toUpperCase();
}
TS מבינה גם return מוקדם, throw, ולולאות - כל מה שמשפיע על flow הקוד.
סיכום¶
- פונקציות צריכות טיפוסים לפרמטרים. טיפוס ההחזרה בדרך כלל מוסק
- פרמטרים אופציונליים עם
?, ברירות מחדל עם=, rest עם... - function overloads מגדירים חתימות שונות לפונקציה אחת
- type assertions (
as) אומרות ל-TS מה הטיפוס - זהירות, אין בדיקה ב-runtime - צמצום (narrowing) מצמצם טיפוס רחב לטיפוס ספציפי
- כלים לצמצום:
typeof,instanceof,in, discriminated unions - type predicates (
is) מאפשרים ליצור פונקציות צמצום מותאמות - בדיקה ממצה עם
neverמוודאת שטיפלנו בכל המקרים - TS עוקבת אחרי הטיפוס לאורך כל ה-control flow