טיפוסים ב TypeScript

21/01/2019

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

1. טיפוסים פשוטים

שפת JavaScript כבר כוללת אוביקטים לתאר טיפוסים פשוטים: string, number, boolean וכמובן null ו undefined. בחרו אחד ו TypeScript כבר תודיע לכם כשאתם מנסים לשים משהו מסוג לא מתאים במשתנה או בפונקציה:

function log(text: string): void {
    console.log(text);
}

// no problem
log('hello');

// sorry - 10 is not a string
log(10);

// I have  "strictNullChecks": true in tsconfig.json
// so this is NOT OK 
log(null);

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

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

function printTimes(text: string, count?:number) {
    if (count == null) {
        console.log(text);
    } else {
        for (let i=0; i < count; i++) {
            console.log(text);
        }
    }
}

// both ok - count is optional
printTimes('hello world');
printTimes('hello world', 7);

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

2. מערכים

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

const arr : string[] = ['one', 'two', 'three'];

// error - arr is an array of strings
arr.push(10);

// error - null is not a string
arr.push(null);

// no problem - String(...) returns a string
arr.push(String(10));

// works - as arr.pop() returns string or undefined
// but the entire expression on the right will always
// return a string
const x : string = (arr.pop() || '');

// this fails - y should never get a string
const y : number = (arr.pop() || 0);

3. ממשקים

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

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

interface Predicate {
    name?: string,
    speed?: number,
    fn?: (_1: string) => boolean,
}

function findBy(options: Predicate) {
    if (options.name) {
        return 'one';
    } else if (options.speed) {
        return 'two';
    } else if (options.fn && options.fn('hello')) {
        return 'three';
    }
}

// no problem - speed is optional
findBy({ name: 'foo' });

// still cool - we can pass both name and speed
findBy({ name: 'foo', speed: 50 });

// we can even pass a function that matches the specified
// signature
findBy({ fn: (x: string) : boolean => x.indexOf('a') >= 0 });

// BUT an object can't have attributes not specified in the interface
findBy({ name: 'foo', type: 'bar' });

// AND the same keys should only have matching values
findBy({ fn: (x: number) => x % 2 === 0 });

4. איחוד וחיתוך

הטיפוסים שראיתם לא הספיקו? מוזמנים לחבר ולחסר אותם כדי ליצור טיפוסים חדשים. אפשר גם לתת שמות לטיפוסים החדשים וכך להרחיב את השפה. הסימן | לוקח מספר טיפוסים ויוצר טיפוס "איחוד", כלומר טיפוס שמאפשר ערך מכל אחד מהטיפוסים (תחשבו על "או"). הסימן & מאפשר לחבר מספר Interface-ים כדי ליצור Interface שכולל את כל השדות מכולם. דוגמאות? כן ברור:

// arr has both strings and numbers
const arr: (string | number)[] = [];

// index 0 in arr might be a string, so x cannot have
// THAT value
const x : number = arr.pop();

// sorry again - arr.pop() may return undefined
const y : (number | string) = arr.pop()

// this works
const z : (number | string | undefined) = arr.pop();

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

type MyArrayItem = string | number;
type MyOptionalArrayItem = MyArrayItem | undefined;

// arr has both strings and numbers
const arr: MyArrayItem[] = [];

// this works
const z : MyOptionalArrayItem = arr.pop();

וכך נראה השימוש ב & כדי לחבר מספר ממשקים לממשק אחד:

interface Point2D {
    x: number,
    y: number,
}

interface NamedItem {
    name: string,
}

type NamedPoint = NamedItem & Point2D;

// Sorry - name is missing
const x : NamedPoint = { x: 10, y: 20 };

// Sorry - x and y are missing
const y : NamedPoint = { name: 'demo' };

// this works
const z : NamedPoint = { x: 10, y: 20, name: 'demo' };

5. ליטרלים

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


type Seven = 7;

// error - x should be 7
const x: Seven = 10;

// Yay
const y: Seven = 7;

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

type Shape = 
    { kind: 'rectangle', width: number, height: number } |
    { kind: 'circle', radius: number } |
    { kind: 'line', a: number, b: number };

// sorry - a circle should have 'radius' and no 'size'
const x: Shape = { kind: 'circle', size: 10 };

// works
const y: Shape = { kind: 'circle', radius: 15 }; 

רוצים למצוא עוד טיפוסים מעניינים? התיעוד של TypeScript הוא מקום מעולה להמשיך את הקריאה. זה הקישור:

https://www.typescriptlang.org/docs/handbook/basic-types.html

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

https://www.tocode.co.il/workshops/63