← Academy Blog

Додаткова плюшка — Typescript на мініпроєкті. Що, де і навіщо?

Всім привіт, Я – Вова, JS розробник в компанії Binary Studio. Мій шлях у Binary, як і у вас, починався з Academy. Етап за етапом, домашка за домашкою, і ось я вже працюю з тими, хто мене навчав. Тема статті була вибрана не випадково, адже в ролі студента, а потім перевіряючого, я проаналізував основні issues під час девелопменту мініпроекту.

Почну з того, що ж являє собою мініпроект. Це проміжний етап між відбором та безпосередньо лекціями Академії. Студентам надається можливість розробити клієнт-серверний додаток (виконати таски аби розвинути стартер). Це повноцінний проєкт, який дасть змогу розробникам поринути в процес реального девелопменту. Команда Binary Studio підготувала стартер використовуючи JS, але якщо уважно вчитатись у Readme, то можна помітити пункт: “Як optional, можна використати TS🤔”. Тож що це таке і куди його приправляти?😋

Typescript - типізований JS👌

Трохи теорії: TypeScript – це надмножина JavaScript, яка компілюється, та приносить опціональну статичну типізацію і деякі можливості сучасних стандартів ECMAScript. Простими словами: ви пишете JS і примішуєте типи:

const getSquareArea = (side) => Math.pow(side, 2);
const getSquareArea = (side: number): number => Math.pow(side, 2);

Особливістю є те, що валідний JavaScript код - це валідний Typescript код (багато хто забуває про це😥). Звідси випливає, що для написання коду на TS в більшості необхідні знання JS і в меншості синтаксис опису типів. Круто чи не так?

Що з типізацією?

js type

Нагадаю, що JavaScript – мова з динамічною типізацією. Це означає, що розробнику не потрібно визначати тип змінної заздалегідь. Тип автоматично визначиться під час виконання програми. Крім цього, можна використовувати одну змінну для зберігання різних типів даних.

let data = 666;
data = { value: 666 };

js generic add

Різниця між цими двома варіаціями типізацій полягає в тому, що у випадку зі статичною перевірка типів здійснюється під час компіляції, тоді як у динамічній – під час виконання інструкцій.

Що може дати Typescript?

Я приведу 3 приклади, де TS наочно може допомогти у розробці мініпроєкту.

  • Виявлення багу на етапі його створення та фікс

    Скористаюсь компонентом PublicRoute, який використовується у нашому thread-js. Його мета доволі зрозуміла - автоматично переадресувати користувачів на задані роути залежно від авторизації, проте зараз не про це😄

import { Navigate, useLocation } from 'react-router-dom';

const PublicRoute = ({ component: Component, user  }) => {
  const location = useLocation();
  return Boolean(user)
    ? <Navigate
        to={{
          pathname:/,
          state: { from: location }
        }}
      />
    : <Component />;
};

Девелопер захотів отримати значення наступного виразу в компоненті, який огортає заданий PublicRoute:

import { Location } from 'react-router-dom';
//
(location.state as { from: Location }).from;

і отримає: reading from from null

Під час написання було допущено помилку, бо state це prop у Navigate, а ніяк не властивість об’єкту, який передається у prop to.

При умові, що налаштовано TS, incorrect structure typing на етапі компіляції девелопер би зрозумів, що

type To = string | Partial<{ pathname: string }>;

interface NavigateProps {
  to: To;
  replace?: boolean;
  state?: {
    from: {
      pathname: string;
    };
  };
}

структура TO може містити в собі лише об’єкт з полем pathname, або ж рядок типу string (в принципі логічно 😂). То ж правильною є реалізація:

import { Navigate, Location, useLocation } from 'react-router-dom';

interface IUserDto {
  id: string;
  name: string;
}

interface IPublicRouteProps {
  component: FC;
  user: IUserDto;
}

const PublicRoute: FC<IPublicRouteProps> = ({ component: Component, user }) => {
  const location: Location = useLocation();
  return Boolean(user) ? (
    <Navigate to={{ pathname: AppRoute.ROOT }} state={{ from: location }} />
  ) : (
    <Component />
  );
};
  • Створення сервісу для постів

class Post {
  constructor({ postRepository }) {
    this._postRepository = postRepository;
  }

  getPosts(filter) {
    return this._postRepository.getPosts(filter);
  }
}

Під час onboarding, та й взагалі просто з часом може бути складно відразу зрозуміти, що являє собою postRepository, який міститься у параметрі getPosts filter, і що повертає метод інстансу класу Post. І це лише мала частина коду, таких ще багато.

interface IPostServiceConstructor {
  postRepository: IPostRepository;
}

interface PostModel {
  id: string;
  body: string;
}

interface IGetPostsByFilterRequestDto {
  from: number,
  count: number;
  userId: number;
}

interface IPostRepository {
  getPosts: (filter: IGetPostsByFilterRequestDto) => Promise<PostModel[] | undefined>
}

class Post {
  #postRepository: IPostRepository ;

  constructor({ postRepository }: IPostServiceConstructor) {
    this.#postRepository = postRepository;
  }

  getPosts(filter: IGetPostsByFilterRequestDto): Promise<PostModel[] | undefined> {
    return this.#postRepository.getPosts(filter);
  }

Тепер стало більш зрозуміло. В цьому прикладі акцентується здатність TS відтворити своєрідну документацію, як все працює.

В доповнення, можна налаштувати IDE так, щоб навівши курсором на змінну чи функцію, можна було дізнатись її біографію, швидко переміщатися між файлами, які містять опис типів, що у свою чергу прискорить процес девелопменту.

jumping files

  • Перейменування actions для reducer`а

Настає такий момент, коли хочеться апгрейднути вже написаний код, і це може погано закінчиться, адже помилиться дуже просто. Наприклад, у нашому мініпроєкті є такий action LOAD_MORE_POSTS, який необхідний у створенні AsyncThunk для отримання списку постів.

import { createAsyncThunk } from '@reduxjs/toolkit';

const ActionType = {
  LOAD_MORE_POSTS: 'thread/load-more-posts',
};
const loadMorePosts = createAsyncThunk(
  ActionType.LOAD_MORE_POSTS,
  async (filters, { getState, extra: { services } }) => {
    const {
      posts: { posts },
    } = getState();
    const loadedPosts = await services.post.getAllPosts(filters);
    const filteredPosts = loadedPosts.filter(
      (post) =>
        !(posts && posts.some((loadedPost) => post.id === loadedPost.id)),
    );
    return { posts: filteredPosts };
  },
);

І розробнику захотілось замінити його на GET_MORE_POSTS. Але при цьому, зміни не збереглись у всіх частинах коду, де це значення використовується.

enum ActionType {
  // LOAD_MORE_POSTS = 'thread/load-more-posts',
  GET_MORE_POSTS = 'thread/get-more-posts',
}

property not exists

Отже, маємо відрефакторений неробочий код😭. TS зможе цьому зарадити. Юзер зрозуміє по помилці в терміналі про невідповідності, що не всюди перейменування спрацювало.

enum properties ide

Крім того, можна налаштувати IDE на підказки, що ще збільшить швидкість таких маніпуляцій і знешкодить баги на самому початку, а не на хот фіксі реліз гілки.👍 Так би мовити, швидкий та більш безпечний рефакторинг. Звісно, обмежусь характеристикою більш безпечний, адже надійність роблять тести.😉

Застереження

По-перше, TypeScript не гарантує надійність і перевірку типів під час виконання програми. Після компіляції, ми маємо справу з чистим JavaScript, і це може зіграти роль при роботі з API. Сервер або ж клієнт може отримати неочікувану для програми структуру даних, що надалі призведе до помилки чи навіть відмови системи. Як один зі шляхів вирішення - можна використовувати shared для бекенду і фронтенду, що певною мірою дасть змогу уникнути невідповідності зі структурами під час спілкування по API. По-друге, при роботі з опціональними властивостями об’єктів можна потрапити в халепу, через те, що розробник свідомо завірив себе, що присутнє значення відмінне від null чи undefined.

Розглянемо наступний приклад коду з нашого thread-js.

interface IImageDto {
  link: string;
}

interface IPostDto {
  body: string;
  image?: IImageDto;
}

interface IPostProps {
  post: IPostDto;
}

const Post: FC<IPostProps> = ({ post }) => {
  return (
    <div>
      {<img src={(post.image as IImageDto).link} alt="post image" />}
      {post.body}
    </div>
  );
};

Компонент Post приймає через пропси об’єкти з доволі логічно назвою post. Завдяки вище прописаним інтерфейсам можна збагнути, що в ньому міститься як optional поле image з відповідною заданою структурою. Використаємо даний компонент для відображення даних. Код скомпілюється як валідний, але працювати не буде якщо властивість image є null.

reading link from null

Тому, про дану перевірку попри TS забувати не можна:

{
  image && <img src={image.link} alt="post image" />;
}

або хоча б таку:

{
  <img src={(post.image as IImageDto)?.link} alt="post image" />;
}

Окрім цього, як уже було зазначено, TS несе за собою опціональну статичну типізацію: дозволяється змінювати рівень строгості типів. Це означає, що змінній можна присвоїти лише визначений тип або загальний такий як any. В такому випадку корисність і надійність зменшується, бо на підсвідомому рівні девелопер все більше довіряє компілятору, у якому відключається жорстка коректна перевірка через any вкидання.

Висновок

В кінці статті хотілось би підкреслити, що кожен пункт в описі тасок на домашці має свою роль, навіть необов’язковий. Не просто так вони даються🙂. Це допомагає розвинутись у нових напрямкам, і Typescript якраз цьому не виключення. Кожна технологія має свої переваги, і тому її застосовують попри всі її недоліки у використанні. З ними потрібно справлятись з правильним підходом, а не просто ігнорувати. TS використовується на багатьох проєктах компанії та є невід’ємною частиною девелоперських буднів. Саме тому мініпроєкт це нагода потренуватись, засвоїти технологію і надалі використовувати коректно на продакшн проєктах.