פיתוח יישומי React ו TypeScript


1. התקנה והגדרות

הדרך הכי קלה לבנות יישום React ו TypeScript היא שימוש ב create-react-app באופן הבא:

$ npx create-react-app myapp --template typescript

2. הגדרת קומפוננטה

בדוגמא הראשונה נרצה לקחת קומפוננטה פשוטה ולהפוך אותה ל TypeScript כדי להתחיל להרגיש את השפה. בשביל הדוגמא כתבתי קומפוננטה בשם Person שלא מקבלת props מבחוץ ולא מחזיקה state. הנה הקוד:

import React from "react";
import "./person.css";

function canVote(age) {
  return age > 18;
}

export default function Person() {
  const age = 18;

  return (
    <p className="person">
      Hi!, I {canVote(age) ? "can" : "can't"} vote
    </p>
  )
}

בשביל להפוך את הקובץ ל TypeScript אני מתחיל בשינוי שם הקובץ ל tsx. אני שומר את השינוי ומיד מקבל את השגיאה:

Parameter 'age' implicitly has an 'any' type.  TS7006

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

// @ts-nocheck

שתגרום ל TypeScript לדלג על בדיקת הקובץ שלכם. כך תוכלו לתקן את הבעיות לאט לאט והקוד עדיין יעבוד גם אם לא כל הבדיקות עוברות. אין עדיין תמיכה בהוספת הערה זו ברמת הבלוק או השורה, למרות שאנשים כבר התחילו לבקש את זה ואפשר לעקוב אחרי הדיון כאן https://github.com/Microsoft/TypeScript/issues/19573.

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

function canVote(age: number) {
  return age > 18;
}

הקוד המלא ב TypeScript נראה כך:

import React from "react";
import "./person.css";

function canVote(age: number) {
  return age > 18;
}

export default function Person() {
  const age = 18;

  return (
    <p className="person">
      Hi!, I {canVote(age) ? "can" : "can't"} vote
    </p>
  )
}

3. הגדרת טיפוסים ל Props

נעבור לקומפוננטה יותר מורכבת ונוסיף ל Person שלנו את היכולת לקבל מבחוץ שם שהוא מחרוזת ואת הגיל שהוא מספר. הקוד ב JavaScript נראה כך:

import React from "react";
import "./person.css";

function canVote(age) {
  return age > 18;
}

export default function Person(props) {
  const { name, age } = props;

  return (
    <p className="person">
      Hi! My name is {name}, and I {canVote(age) ? "can" : "can't"} vote
    </p>
  )
}

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

Hi! My name is , and I can't vote

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

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

function canVote(age: number) {
  return age > 18;
}

השגיאה השניה היא על המשתנה props:

Parameter 'props' implicitly has an 'any' type.  TS7006

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

export default function Person(props: {
    name: string,
    age: number,
}) {
  const { name, age } = props;

  return (
    <p className="person">
      Hi! My name is {name}, and I {canVote(age) ? "can" : "can't"} vote
    </p>
  )
}

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

Type '{}' is missing the following properties from type '{ name: string; age: number; }': name, age  TS2739

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

<Person name="mike" age="10" />

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

Type 'string' is not assignable to type 'number'.  TS2322

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

<Person name="mike" age={10} />

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

4. הגדרת ערכי ברירת מחדל ל Props

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

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

function canVote(age: number) {
  return age > 18;
}

export default function Person(props: {
    name: string,
    age: number,
    backgroundColor?: string,
}) {
  const { name, age, backgroundColor } = props;

  const style = backgroundColor ? { backgroundColor } : {};

  return (
    <p className="person" style={style}>
      Hi! My name is {name}, and I {canVote(age) ? "can" : "can't"} vote
    </p>
  )
}

אפשרות שניה היא להגדיר defaultProps על פוקנציית הפקד. זה נראה ככה:


export default function Person(props: {
    name: string,
    age: number,
    backgroundColor: string,
}) {
  const { name, age, backgroundColor } = props;

  const style = { backgroundColor }

  return (
    <p className="person" style={style}>
      Hi! My name is {name}, and I {canVote(age) ? "can" : "can't"} vote
    </p>
  )
}

Person.defaultProps = {
  backgroundColor: 'forestgreen',
};

5. הגדרת טיפוסים ל State

אנחנו נשארים עם קומפוננטת ה Person אבל הפעם רוצים לאפשר לאנשים גם לגדול: הגיל שמתקבל מבחוץ יהיה הגיל הראשוני של הפקד, ולחיצה על כפתור Grow Up בתוך הקומפוננטה תעלה את הגיל ב-1.

שינוי השם מתוך ה IDE הוא ממש פשוט - באופן אוטומטי TypeScript מצליח לעדכן את כל המקומות שמשתמשים במאפיין זה לשם החדש. בשביל להוסיף משתנה לסטייט אני משתמש ב useState Hook באופן הבא:

const [age, setAge] = useState(initialAge);

סוג המשתנה age יהיה מספר ונגזר בצורה אוטומטית מסוג הערך שהעברתי ל useState. אם אתם רוצים להיות יותר יצירתיים אפשר להעביר את סוג המשתנה בתור Generic באופן הבא:

const [age, setAge] = useState<number|null>(initialAge);

קוד הקומפוננטה המלא יראה כך:

export default function Person(props: {
    name: string,
    initialAge: number,
    backgroundColor: string,
}) {
  const { name, initialAge, backgroundColor } = props;
  const [age, setAge] = useState(initialAge);

  const style = { backgroundColor };

  function growUp() {
      setAge(age => age + 1);
  }

  return (
      <div className="person" style={style}>
          <p>
              Hi! My name is {name}, and I {canVote(age) ? "can" : "can't"} vote
          </p>
          <p>
              By the way I'm currently {age} years old...
              <button onClick={growUp}>Grow Up</button>
          </p>
      </div>
  )
}

6. אירועים

ריאקט משנה את סוגי האירועים והופכת את אירועי ה HTML הרגילים לאירועים סינטטיים של ריאקט. בהגדרות הטיפוסים של TypeScript יש לנו בצורה אוטומטית תמיכה באירועים החדשים - כל האירועים נמצאים בתוך מרחב השמות של ריאקט.

ניקח את אלמנט ה JSX הבא כדוגמא:

import React, { useState } from 'react';

export default function TextBoxes(props) {
  const [text, setText] = useState('');
  function handleChange(e) {
    setText(e.target.value);
  }

  return (
    <div>
      <input type="input" value={text} onChange={handleChange} />
      <input type="input" value={text} onChange={handleChange} />
      <input type="input" value={text} onChange={handleChange} />
      <input type="input" value={text} onChange={handleChange} />
      <input type="input" value={text} onChange={handleChange} />
    </div>
  )
}

הפונקציה handleChange מניחה שאתם יודעים טוב HTML ולא טועים באיות. היא מקבלת משתנה מסוג e ומשתמשת בשדה target.value שלו. מה זה e? ומי זה target.value? את זה הקוד לא מספר לנו.

במעבר ל TypeScript החיים הופכים יותר ברורים:

import React, { useState } from 'react';

export default function TextBoxes(props: {}) {
  const [text, setText] = useState('');
  function handleChange(e: React.ChangeEvent<HTMLInputElement>) {
      setText(e.target.value);
  }

  return (
    <div>
      <input type="input" value={text} onChange={handleChange} />
      <input type="input" value={text} onChange={handleChange} />
      <input type="input" value={text} onChange={handleChange} />
      <input type="input" value={text} onChange={handleChange} />
      <input type="input" value={text} onChange={handleChange} />
    </div>
  )
}

עכשיו האירוע הפך ל React.ChangeEvent על HTMLInputElement. זה כבר אירוע מאוד ספציפי שמגדיר בדיוק מה השדות עליו ומה הסוגים שלהם כך שלא נצטרך להתבלבל בין value ל textContent או לשדות אחרים.

7. ספריות צד-שלישי

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

$ yarn add lodash @types/lodash

אבל בתמורה נקבל את ההגדרות המתאימות כדי שנוכל להשתמש נכון בספריה.

ספריות צד-שלישי אחרות מספקות בעצמן את הגדרות הטיפוסים שלהן - כך לדוגמא הספריה axios מכילה את הקובץ index.d.ts עם כל ההגדרות, כמו שאפשר לראות במאגר שלהם בגיטהאב בקישור https://github.com/axios/axios/blob/master/index.d.ts.

גם אם נפלתם על ספריית צד שלישי שאין לה קבצי הגדרות עדיין תוכלו לשלב אותה ביישום ה TypeScript שלכם, רק תצטרכו לייצר קובץ הגדרות בעצמכם (אבל אל דאגה - הקובץ יכול להיות כמעט ריק). אם אתם במצב כזה צרו קובץ בתיקיה הראשית של הפרויקט בשם global.d.ts ובתוכו השורה:

declare module 'google-spreadsheet';

כמובן במקום google-spreadsheet כתבו את שם המודול שאתם צריכים שלא מצאתם עבורו קובץ הגדרות.

8. שימוש ב Ref

אחד הדברים שקצת מבלבלים אנשים במעבר ל TypeScript הוא היחס של השפה ל null-ים ובדיקות האם משהו הוא null. דוגמא בולטת מהעולם של ריאקט היא העבודה עם ref. קחו לדוגמא את הקומפוננטה הבאה:

import React, { useRef } from 'react';

export default function Focusable(props: {}) {
    const myText = useRef<HTMLInputElement>(null);

    function focus() {
        myText.current.focus();
    }

    return (
        <div>
            <input type="text" ref={myText} />
            <button onClick={focus}>Focus</button>
        </div>
    )
}

הקוד לא מתקמפל כי myText.current עשוי להיות null. טוב זה ברור: לפני ה render, או אם ב render לא ניתן ערך לאלמנט אז יש שם null. אנחנו אולי יודעים שזה בחיים לא יקרה, אבל ל TypeScript אין דרך לדעת את זה.

פיתרון אופציונאלי אחד הוא Casting - כלומר להגיד בעצמנו ל TypeScript שאף פעם אין null במשתנה. זה נראה ככה וממש לא מומלץ:

// DON'T DO THIS

function focus() {
    (myText.current as any).focus();
}

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

    function focus() {
        myText.current?.focus();
    }

שימו לב גם להפעלה של useRef - ה hook הוא גנרי והעברתי לו בבנאי את סוג האלמנט שצפוי להישמר בו. בצורה כזאת TypeScript יכול לעזור לי להבין איזה שדות קיימים על myText.current, כמובן אחרי שוידאתי שהוא לא null.

9. קריאת המשך

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

הספר TypeScript Handbook מהתיעוד של השפה הוא המקום הכי טוב ללמוד על השפה עצמה. כתוב בצורה מעניינת ועם המון דוגמאות: https://www.typescriptlang.org/docs/handbook/basic-types.html

המדריך בקישור הבא כולל ספרון על שילוב TypeScript וריאקט ומכיל דוגמאות קצת אחרות מאלה שהופיעו בוובינר שלנו: https://fettblog.eu/typescript-react/

והדף React TypeScript CheatSheet הוא ענק ויוביל אתכם לעוד 2-3 דפי קיצורים שכוללים רשימה מפורטת של כל התבניות לחיבור בין ריאקט ל TypeScript וגם אינספור בעיות שאתם עשויים להיתקל בהן עם פיתרונות והסברים. קריאה מומלצת למרות שקצת ארוכה: https://github.com/typescript-cheatsheets/react-typescript-cheatsheet