MasterScripter

המדריך השלם ל – TypeScript: חלק ב'

הגעתם רק עכשיו? אל תדלגו על חלק א' של המדריך השלם ל – TypeScript!
החלטתם אחרת? על אחריותכם בלבד! 😉

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

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

עוד קצת על מחלקות ב – TypeScript..

פונקציות עדכון / גישה לתכונה

(באנגלית זה נשמע יותר טוב: Mutator / Accessor Method)

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

היישום של הפונקציות הללו נעשה באמצעות שתי מילים שמורות: get לשליטה על גישה לערך תכונה ו – set לשליטה על עדכון ערך תכונה.

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

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

בחלק הזה, הוגדרו שתי תכונות מחלקה:

  • _dateEntry
  • format

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


אלה מכם שעוקבים אחרי הפוסטים שלי בטח מזדעקים – איך מישהו שכתב את הפוסט PHP – לכתוב את זה נכון מרשה לעצמו להכריז על משתנה private אחד עם קו תחתון, ומשתנה private שני בלי קו תחתון?!
התשובה – ממש בהמשך 🙂

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

  1. פונקציית get לא יכולה לקבל פרמטרים
  2. פונקציית set יכולה לקבל פרמטר אחד בלבד
  3. סוג הערך שמחזירה פונקציית ה – get חייב להיות לסוג הפרמטר שפונקציית ה – set מקבלת

במידה ונגדיר עבור משתנה פונקציית get בלבד, המהדר יתייחס אליו אוטומטית כ – readonly כלומר – משתנה שיכול לקבל ערך רק פעם אחת, במעמד יצירת המופע.


אני יודע, אני יודע.. אתם כבר לא עומדים במתח.
משתנה private אחד עם קו תחתון ומשתנה private שני בלי קו תחתון?! הסיבה לכך פשוטה:
המגמה כיום היא לא להשתמש בקו תחתון כמקדם ל – private, וניתן לראות שגם יוצרי TypeScript מקפידים על כך בדוקומנטציה.
עם זאת, מכיוון ש get ו – set מוגדרים פרטנית למשתנה (בניגוד ל – PHP למשל),
אם לא "נסמן" את המשתנה, יווצר מצב שבו יש לנו תכונה ופונקציה זהה. ועל זה, המהדר לא ישתוק.

תכונות סטטיות

TypeScript מאפשרת הכרזה על תכונות סטטיות, באמצעות המילה השמורה static.
כל תכונה יכולה להיות בנוסף גם private / protected / public, readonly וכו'.

(הדוגמה נלקחה מהדוקומנטציה של TypeScript ונערכה)

בקטע הקוד מוכרז משתנה סטטי origin, והגישה אליו נעשית באמצעות שם המחלקה ולא this.
אם ננסה להשתמש ב – this, נקבל את השגיאה הבאה:

Property 'origin' does not exist on type 'Grid'.

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

מחלקות מופשטות

ב – TypeScript, מחלקה מופשטת (Abstract Class) פועלת לפי אותו עקרון כמו במרבית שפות התכנות: היא מחלקה שלא ניתן ליצור מופעים מסוגה והיא מיועדת להיות בסיס למחלקות אחרות,
כשההבדל בין מחלקה מופשטת לממשק טמון בכך שבמחלקה מופשטת ניתן לממש פונקציות, בניגוד לממשק.

נכריז על מחלקה או על פונקציה כמופשטת באמצעות המילה השמורה abstract:

אם נעז לאתגר את המהדר ע"י ניסיון ליצור מופע מסוג Greeter, מיד הוא ינזוף בנו:

Cannot create an instance of the abstract class 'Greeter'

שימוש במחלקה כממשק

מכיוון שכאשר אנו מגדירים מחלקה, אנחנו גם מגדירים סוג נתונים, TypeScript מאפשרת להתייחס למחלקה כממשק, בין אם כבסיס לממשק אחר או כיישום במחלקה:

סוג נתונים גנרי

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

הגדרת סוג נתונים גנרי יכולה להתבצע עבור: פונקציות, ממשקים, מחלקות ותכונות מחלקה.

פונקציה גנרית

התחביר דומה מאד ל – C++, אבל גם מתכנתי C# ו – JAVA ימצאו בו משהו מוכר.
הפונקציה מוגדרת כגנרית ע"י הצמדת <T> לשמה: המשמעות בפועל היא שללא ההצמדה, המהדר לא יכיר בסוג נתונים T והפונקציה לא תוכל להיות גנרית.

ממשק גנרי

ככל הנוגע לממשקים (וגם למחלקות), ניתן להגדיר את הפרמטר הגנרי בחתימת הממשק:

וכך הסוג יהיה נגיש עבור כל חברי הממשק.

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

מחלקה גנרית

העקרון זהה לממשקים, הגדרת הפרמטר תיעשה או בחתימת המחלקה:

או בפונקציה עצמה:

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

מכיוון שהמהדר מתעלם מסוג הנתונים any, הוא לא יזהיר אותנו מהאפשרות שלפרמטר arg אין תכונה בשם length,
והשגיאה תתגלה רק בזמן ריצה. זה לא טוב או רע, זו ההתנהגות המצופה מסוג נתונים any.


E..V..T.. מה קורה פה?
שם הפרמטר הגנרי (זה שנמצא בין החצים <>) ניתן לקביעה ע"י המתכנת ואין לו משמעות אמיתית.
אז למה אתם נתקלים באותן אותיות אתם שואלים? מדובר במוסכמה ולא בחובה, קבלו את המשמעות שלהם:

T: סוג (Type)

E: אירוע (Event)

V: ערך (Value)

סוג נתונים גנרי מרחיב

כאשר נרצה לחייב את סוג הנתונים הגנרי לעמוד בדרישות סף, נוכל להגדיר אותו כמרחיב ממשק באמצעות המילה השמורה extends:

כעת, סוג הנתונים T חייב לקיים את הממשק Example.

סוגי נתונים מורכבים

סוג נתונים מוצלב

(באנגלית: Intersection Type)

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

בשביל להבין יותר טוב את כוונת המשורר, ניזכר בתהליך ההאבקה.
כן כן, אותו תהליך שלמדנו עליו ביסודי (והמשוגעים לדבר גם בתיכון ו.. באוניברסיטה?!) – תהליך רביית הצמחים, ותודה לשותפי המשכיל @שלומקה על הרעיון לדוגמה!
אם יש ביולוגים בקהל, אני מתנצל מראש על השגיאות הלוגיות, הפטאליות, כתיב וכל שאר השגיאות שאעשה בתיאור תהליך ההאבקה!

בתהליך ההאבקה משתתף הצמח (Plant, בקטע הקוד) וחרק, במקרה שלנו – דבורת דבש (HoneyBee).
הצמח, מחד, מייצר צוף – שמכיל בתוכו את האבקנים המשמשים לרבייה
הדבורה, מאידך, יונקת את הצוף ומעבירה אותו לתא הרבייה של הצמח.

ועכשיו, מעברית לקוד:

נפרק את קטע הקוד לשלושה חלקים:

extend

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

  • הפונקציה מקבלת כפרמטרים שני סוגים גנריים, <T, U>
  • הפונקציה מחזירה עצם מסוג T וגם U – עצם מסוג נתונים מוצלב.
    איחוי סוגי הנתונים נעשה באמצעות האופרטור &

Plant ו – HoneyBee

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

pollination

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

סוג נתונים משולב

(באנגלית: Union Types)

TypeScript מציעה להתמודד עם מצבים בהם סוג הנתונים לא ידוע, אך כן ידוע אילו סוגי נתונים הוא יכול להיות.
ניחשתם נכון.. זה נקרא סוג נתונים משולב, והוא מאפשר למהדר מצד אחד לתאום את המציאות (כמו שאמרתי לא פעם, TypeScript עובדת טוב עם ספריות VanillaJS) אך מצד שני לא לוותר על עצמו, כמו שקורה עם סוג המשתנים any.
התחביר להגדרת סוג נתונים משולב פשוט מאד: type1 | type2 | type3 | ...
רוצים דוגמה? קיבלתם
בקטע הקוד הבא מתוארת מחלקה שמריצה פקודות באמצעות ממשק שורת פקודה (CLI) – הממשק מאפשר הכנסת פקודה בכל פעם, או מערך של פקודות.

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

Argument of type 'number[]' is not assignable to parameter of type 'string | string[]'

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

סמלים

(באנגלית: Symbols)
לפני שאסביר על סוג הנתונים עצמו, חשוב לציין שמדובר בסוג נתונים חדש, שהתווסף לתקן ECMAScript 2015,
והמהדר ידע לעבוד איתו רק אם נגדיר לו שאנחנו עובדים לפי התקן es6:

(הסבר על הגדרות מהדר בחלק א' של המדריך)

בניגוד למחלקות, ממשקים וכו' שמומרים לקוד JS שנתמך באופן רחב, סוג הנתונים החדש לא נתמך בכלל ב – IE על כל גרסאותיו, וחלק מתכונותיו לא נתמכות גם באופרה ובספארי. (המידע נלקח מ – mozilla.org. לטבלת תאימות לדפדפנים של סוג הנתונים). בנוסף, היישום של סמלים ב – TypeScript הינו חלקי, ככל הנראה בשביל למנוע אי-תאימות ככל הניתן.
אני, באופן אישי, משתדל בשלב זה לא להשתמש בסוג הנתונים הזה מכיוון שאיננו נתמך באופן מלא באחוז גבוה מספיק של דפדפנים, אבל מצד שני אני חושב שסוג הנתונים הזה טומן בחובו המון פוטנציאל לעתיד ולכן כדאי להבין אותו. אז קדימה:

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

הכוונה בבלתי ניתן לשינוי ברורה, אבל למה התכוון המשורר כשכתב "בעל ערך ייחודי"?
בשביל להבין לעומק את ההגדרה הזו, נסתכל על קטע הקוד הבא, שמתאר השוואת מחרוזות אל מול השוואת סמלים:

אם תבחרו להאמין לקטע הקוד למעלה, אז נגיע למסקנה: כל סמל הוא ייחודי. (ביקר לי – לא עובד עליכם!).
שתי נקודות בנוגע לסוג הנתונים:

  • ב – ECMAScript 2015 ניתן לבצע השוואה בין שני סמלים, אבל זה לא קיים ב – TypeScript נכון לגרסה הנוכחית.
  • הערך שמועבר כפרמטר לבנאי הוא בגדר רשות, ומשמש כתיאור פנימי של הסמל, שיופיע בעת המרה למחרוזת, שימוש ב – console.log() ובהודעות שגיאה

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

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

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

וכאן באים לידי ביטוי הסמלים. זוכרים מה אמרנו עליהם? הנה תזכורת:

  1. סמל הינו בלתי ניתן לשינוי מרגע יצירתו
  2. סמל הינו ייחודי – לעולם לא יהיה שווה לסמל אחר

בואו נערוך את הדוגמה, רק שהפעם נשתמש בסמל במקום במחרוזת כמזהה:

ובכך הפכנו את הסקריפט שלנו לחסין מהתנגשויות, יהיו אשר יהיו.

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

 

לולאת for..of

פועלת בדומה ללולאת for..in  המוכרת, עם שינוי אחד קטן אך משמעותי. אתם הולכים לאהוב את זה:

כידוע, לולאת for..in עוברת על אינדקסים ולא על איברים:

הלולאה הזו אחראית לטעות נפוצה בקרב מפתחי JS, שמצפים לקבל את הערך, ובמקום זאת מקבלים את האינדקס.


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

וכאן באה לעזרנו לולאת for..of, שעוברת על איברים:

לא רע, נכון? 😉

הטוב

ב – es6, לולאת for..of יכולה לרוץ לא רק על מערכים, אלא על כל עצם שמוגדר כניתן לחִזְרוּר.
בנוסף, אנחנו יכולים להגדיר בעצמנו סוג נתונים שיהיה ניתן לחִזְרוּר. אבל זה כבר נושא שלם שקשור יותר ל – es6 מאשר ל – TypeScript,
ואכתוב עליו בעתיד, כשהשימוש בתכונות של es6 יהיה רווח יותר.

הרע

es6 עדיין לא נתמך בצורה רחבה מספיק ועבור אנשים רבים (ביניהם אני), זו הייתה הסיבה מלכתחילה לעבור ל – TypeScript.
כאשר נגדיר במהדר את התקן ל – es5 ומטה, נוכל להשתמש ב – for..of רק עבור סוגי הנתונים string ו – array, וכשננסה להשתמש בלולאה עבור סוג נתונים אחר, זה בדיוק מה שהמהדר יגיד לנו:

Type 'number' is not an array type or a string type.

זאת מכיוון שבקוד ה – JS המומר, נעשה שימוש בתכונה length, שקיימת רק במחרוזות ומערכים.

מודולים

כברירת מחדל, קוד שנכתב ב – TypeScript נמצא במרחב גלובלי.
המשמעות של כך בפועל היא שאם נכריז על משתנה בקובץ X, הוא יהיה זמין גם בקובץ Y, מה שעלול לגרום (קרוב למדי) לתרחישים הבאים:

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

מודולים יוצרים מרחבים מקומיים ומבודדים, והם הדרך המומלצת לכתוב TypeScript ובכלל – לכתוב Javascript.
קובץ TypeScript בעצם משמש כמודול, בתנאי שהוגדרו בתוכו רכיבים. ואיך מגדירים רכיבים אתם שואלים? יש לי את התשובה שאתם מחפשים!

 

export

כדי להגדיר אלמנט (משתנה / מחלקה / ממשק / פונקציה / …) כרכיב של המודול, נשתמש במילה השמורה export:

כעת, האלמנטים הללו לא יהיו זמינים בקבצי .ts אלא אם נייבא אותם.

ניתן גם לייצא רכיב בשם שונה מזה שניתן לו בקובץ המקור:

 

אחרון חביב, רכיב ברירת מחדל נגדיר באמצעות המילה השמורה default

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

import

כדי לייבא רכיבי מודול מקובץ אחד לשני, נשתמש במילה השמורה import:

נקודות חשובות בנוגע לקובץ המיובא שיחסכו לכם הרבה כאב ראש:

  • אין צורך לכלול את סיומת הקובץ .ts
  • מומלץ להשתמש במיקום יחסי (./) מאשר במיקום אבסולוטי, אך המהדר יודע לחפש את הקובץ גם אם לא ניתן מיקום יחסי. לקריאה נוספת

כפי שניתן לייצא רכיב בשם שונה, כך ניתן גם לשנות את שם הרכיב עבור הקובץ המייבא, בעת הייבוא:

וגולת הכותרת: ייבוא כלל הרכיבים של מודול, על-ידי שימוש בתו השמור *:

מתחם שמות

(באנגלית: Namespace)

ב – TypeScript, מתחם שמות מאפשר לאגד אלמנטים תחת שם אחד ובעצם מהווה תחליף תחבירי נוח יותר לתחביר ה – JavaScriptי.
אם ב – Javascript, נכתוב זאת כך:

ב – TypeScript, כל שעלינו לעשות הוא להשתמש במילה השמורה namespace:

כעת, כל מה שנכתוב בתוך הבלוק ישתייך למתחם הנתונים Validate.
בדומה למודולים, גם במתחמי נתונים ניתנת לנו האפשרות להחליט אילו אלמנטים יהיו 'ציבוריים'- ניתנים לשימוש מחוץ להגדרת ה – namespace, באמצעות המילה השמורה export:

כמובן שניתן לבצע export לא רק על פונקציות, אלא על הכל: משתנים, פונקציות, ממשקים, מחלקות.
הקוד יומר לקוד JS בתבנית שהראתי למעלה.

הפעם, באמת סיימנו..

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

ישנם עוד מס' נושאים שלא כיסיתי, ואני מקווה שבמעלה הדרך יזדמן לי לכתוב עליהם בפוסטים נפרדים.
עד למדריך השלם הבא 🙂

מקורות

  1. בסיס למדריך וקטעי קוד – תיעוד רשמי באתר השפה
  2. הסבר על סוג נתונים גנרי בשפה – Coding Defined
  3. Type System – הסבר על סוגי נתונים ב – TypeScript
  4. השראה לקטעי קוד – Ryan Cavanaugh
  5. הסבר על סוג נתונים "סמלים" – Jason Orendorff

אם הגעת עד לפה, הגיע הזמן להירשם לניוזלטר!

Summary
Article Name
המדריך השלם ל - TypeScript: חלק ב'
Description
חלק ב' של מדריך מקיף הכולל הסבר על שפת התכנות TypeScript או בשמה המקוצר: TS.
Author
Publisher Name
MasterScripter
Publisher Logo
אודות 
hello world! אני יונתן, מתעסק בתכנות ופיתוח WEB פלוס מינוס מאז שאני זוכר את עצמי. ב2013 הקמתי את קבוצת סיזן ביחד השותף היקר שלי - שלומי שלומקה זק. ביום מתעסק בפיתוח ועיצוב אתרי אינטרנט ואפליקציות, עיצוב חוויית משתמש, הקמת מיזמים, ועוד..אבל מה שלא ידעתם עליי - זה שבלילה אני באטמן. אל תגלו לאף אחד!

0 תגובות

השאר/י תגובה

האימייל לא יוצג באתר. שדות החובה מסומנים *