Введение

Решим реальную практическую задачу, с которой мне пришлось столкнуться на моем проекте React/TypeScript.

Задача

У нас есть массив конкретных строковых значений, таких как "first", "second", "third", "fourth" и "fifth". Необходимо отобразить их на странице, т.е как-то использовать, а также убедиться, чтобы эти данные были строго типизированы и TypeScript нам выдавал всплывающие подсказки при их использовании в коде.

Решение

Сначала поместим данные в неизменяемый (readonly) массив:

const numberNames = ["first", "second", "third", "fourth", "fifth"] as const
  • Мы объявили константу numberNames и присвоили ей массив строковых литералов;

  • Конструкция as const используется чтобы сделать из данного массива неизменяемый кортеж (tuple), где конкретные строковые литералы сохраняются как литеральные типы;

  • Сохранение строковых литералов в качестве литеральных типов означает, что компилятор TypeScript рассматривает каждый элемент в массиве как отдельный тип, соответствующий его точному строковому значению. Например, тип первого элемента 'first' не является просто общим строковым типом (string), а конкретно типом 'first';

  • Убедитесь в этом сами, создав два массива - один с as const, а другой без. А затем наведите на переменную и посмотрите разницу.

Теперь создадим тип на основе этого массива:

type NumberName = typeof numberNames[number]
  • Здесь мы создали тип NumberName, значением которого является объединение (union) типов строковых литералов из массива;

  • Индексатор числа [number] извлекает объединенный тип отдельных элементов массива. В результате typeof numberNames[number] представляет собой объединенный тип элементов массива:

"first" | "second" | "third" | "fourth" | "fifth"
  • Почему [number], а не, например [string]? Все просто - как мы знаем, к элементам массива можно получить доступ через числовой индекс: например, numberNames[0] => "first";

  • Даже если бы у нас был массив со значениями других типов, например [1, 'blabla', 555], то в любом случае мы бы использовали [number] для того, чтобы создать тип объединения на его основе 1 | 'blabla' | 555;

  • Кстати, данная возможность извлечения типа из кортежа при помощи индексатора [number], как у нас в примере, появилась в версии TypeScript 4.1.

Заключение

Для чего это все нужно - чтобы строго типизировать данный список строк и когда мы будем с ним работать, получать такого рода подсказки:

Я выбрал выбор типов (Type inference) и as const в качестве решения, потому что хотел реализовать его максимально гибким и с минимальным количеством кода.

P.S. Это мои первые шаги в области написания веб-статей, и это соответственно моя первая статья. Поэтому я хотел начать с чего-то небольшого, чтобы поделиться полезной информацией со всеми вами. Буду рад вашим комментариям. Спасибо!

Комментарии (23)


  1. dopusteam
    24.07.2023 17:51
    +4

    Не проще ли было сделать?

    type MyType ="first" | "second" | ...


    1. dlc
      24.07.2023 17:51
      +5

      Не проще по двум причинам:

      1) В таком случае у вас уже есть обычный массив с данными и его можно использовать, как массив во вс\м коде.

      2) Мы просто выводим тип из этого массива и этот тип всегда будет синхронизирован с реальными данными в массиве.

      По факту эдакая замена enum, но с расширенными структурами данных.


      1. mavludin Автор
        24.07.2023 17:51

        Спасибо за разъяснение - так и есть, можно сделать и иными способами, но я конкретно рассматривал решение с наименьшим количеством кода.

        Ну и если массив в будущем будет дополняться, неудобно же все вручную прописывать при помощи enum или type MyType ="first" | "second" | ...


        1. dopusteam
          24.07.2023 17:51

          Я б это в статью добавил, хорошие аргументы


          1. mavludin Автор
            24.07.2023 17:51

            Просто если расписывать разницу с enum, статья растянется и это изначально не являлось целью этой статьи.


    1. Kirill-112
      24.07.2023 17:51
      +4

      Приходится повторяться потом:

      const arr: MyType[] = ["first", "second", ...];


  1. UP_IM
    24.07.2023 17:51

    А почему не использовать enum для этих целей?


    1. mavludin Автор
      24.07.2023 17:51

      Можно и enum, но я конкретно рассматривал решение с наименьшим количеством кода.

      Ну и если массив в будущем будет дополняться, нам придется в enum еще лезть и дополнять все вручную.


      1. UP_IM
        24.07.2023 17:51

        Так же как и в вашем случае, тоже придется дополнить варианты возможных значений))


        1. mavludin Автор
          24.07.2023 17:51

          Я это делаю один раз на месте в массиве


        1. mavludin Автор
          24.07.2023 17:51

          С enum вот так например:

          enum NumberNamesEnum {
            First = "first", 
            Second = "second", 
            Third = "third", 
            Fourth = "fourth", 
            Fifth = "fifth", 
          }
          
          const numberNames: NumberNamesEnum[] = [
            NumberNamesEnum.First, 
            NumberNamesEnum.Second, 
            NumberNamesEnum.Third, 
            NumberNamesEnum.Fourth, 
            NumberNamesEnum.Fifth, 
          ];

          И это все надо держать в актуальном состоянии, и потом чтоб использовать - импортировать везде.
          Но с другой стороны такое выглядит лучше задокументированным и в сравнении с моим решением не нужно голые строки везде использовать.


          1. ubx7b8
            24.07.2023 17:51

            Конкретно в этом случае можно сделать:

            const numberNames: NumberNamesEnum[] = Object.values(NumberNamesEnum);

            но если сделать "const enum", то так уже не получится.


            1. Kirill-112
              24.07.2023 17:51
              +4

              Пожалуйста, не надо использовать енумки в качестве объектов. Они не предназначены для этого.


    1. serginho
      24.07.2023 17:51
      +2

      Как минимум один плюс: для передачи литерала в качестве параметра вам не нужно дополнительно импортировать enum и искать, откуда его импортировать.


    1. azizoid
      24.07.2023 17:51
      +2

      enum очень ужасен, аж один из разработчиков тайпскрипта говорил что есои бы пришлось изобретать enum сейчас то они не стали бы это делать.
      Вместо enum многие (и я) пользуются объектами с последующим `keyof typeof numberNames`


      1. mavludin Автор
        24.07.2023 17:51

        Да, в случае с объектом так и есть


  1. arusakov
    24.07.2023 17:51

    А может еще добавите в статью, в какой версии TypeScript появилась возможность вот так использовать "индексатор числа [number]". Это может быть полезно для тех, кто внимательно следил за TypeScript раньше, а сейчас немного ослабил хватку :)


    1. mavludin Автор
      24.07.2023 17:51

      Хорошая идея. Спасибо! Добавил инфу в пост.


      1. arusakov
        24.07.2023 17:51

        Спасибо!


  1. SWATOPLUS
    24.07.2023 17:51
    +2

    Спасибо. Рад в кое-то веке видеть небольшую, но информативную техническую статью на хабре.

    Так же хочу показать ещё один интересный пример:

    const args = [8, 5] as const;
    const angle = Math.atan2(...args);
    

    Так как к массиву применен as const его длинна известна на стадии компиляции. Функция Math.atan2 принимает только два аргумента, и, так как компилятор знает о длине массива, позволяет использовать spread-оператор. Если убрать as const, то компилятор не будет знать о длине массива, и не разрешит такой вызов.


    1. evgenyidf
      24.07.2023 17:51

      Круто, мне понравилось использование таким образом


  1. FluffyArt
    24.07.2023 17:51

    Удивительно, что на две бытовых строчки кода такая объемная статься вышла. И название такое, что думал что-то необычное увидеть

    Но совсем уж начинающим может пригодиться, жаль, что такую статью через Гугл не найти, слишком сложное для этого название. На вашем месте, я бы упростил

    Кому интересно, какие плюсы у этого подхода:

    1. Если смотреть фронтенд - не надо импортить везде как enum, и что ещё хуже добавлять в контекст компонента для того, чтобы он попал в рендер (привет vue2)

    2. Удобно описать валидатор для конечной точки в цепочке компонентов (функций). В цепочке проверяем тип (и удобно указываем значения), в конце проверяем, что никто не навлевал на требуемый тип, ибо ts не гарантия :)


    1. mavludin Автор
      24.07.2023 17:51

      Я гуглил и смотрел какой перевод у понятия type inference. Везде было "Вывод типов". Решил так и использовать, хотя может можно было что-то типа "Вычисление типа по массиву строковых значений".

      Статья вышла в таком объёме, в каком я сам решил для аудитории тех, кто с таким подходом не знаком, с одной стороны стараясь не поливать воду, а с другой стараясь ничего не пропустить и простым языком дать объяснение.