Всем привет???? В последние годы среди фронтенд разработчиков Typescript используется практически везде по умолчанию, начиная небольшими пет-проектами и заканчивая огромнейшими веб-приложениями. Однако, до сих пор на некоторых проектах можно встретить кучу any и Function. Давайте разберемся используете ли вы этот невероятно мощный инструмент правильно?

Дженерики - всему голова

Используйте их везде, где только возможно. Это поможет лучше определять используемый тип в коде. К сожалению, их не часто используют, а зря.

function returnType<T>(arg: T): T {
  return arg;
}

returnType<string>('Habr') // всё ок
returnType<number>('Habr') // ошибка
// ^ Argument of type 'string' is not assignable to parameter of type 'number'.

Если используете какой-то определённый тип, то обязательно используйте extends:

type AddDot<T extends string> = `${T}.` // получает только строки, иначе ошибка

Тернарные операторы и extends

extends - очень полезная вещь, помогает определить от чего наследуется тип по иерархии типов (any -> number -> ...) и сделать сравнение.

Благодаря комбинации extends и тернарным операторам можно создавать условные конструкции:

type IsNumber<T> = T extends number ? true : false
IsNumber<5> // true
IsNumber<'lol'> // false

infer

Это ключевое слово даёт невероятную гибкость при использовании Typescript. Не стоит его бояться, относитесь к нему, как к новой переменной. Рассмотрим пример:

type GetString<T extends string> = T extends `${infer R}.` ? R : ''

Этот тип очень простой, он берёт передаваемую строку и создаёт тип (переменную) R из T, затем выдаёт его.

GetString<'Hi.'> // 'Hi'

readonly

Используйте readonly по умолчанию, это позволит избежать случайного перезаписывания типов в вашем интерфейсе.

interface User {
  readonly name: string;
  readonly surname: string;
}

as const

Допустим, у вас есть массив, который приходит с бекэнда [1, 2, 3, 4] и вам нужно использовать только эти четыре числа, то есть сделать массив иммутабельным. С этим запросто справится конструкцияas const:

const arr = [1, 2, 3, 4] // сейчас тип number, можно передать любое число
arr[3] = 5 // [1, 2, 3, 5]

const arr = [1, 2, 3, 4] as const
// теперь тип представлен как readonly [1, 2, 3, 4]
arr[3] = 5 // ошибка

Satisfies

Появился относительно недавно (в версии 4.9), но уже завоевал мою любовь тем, что позволяет наложить ограничения, не изменяя тип. Это бывает очень полезно, когда вы используете разные типы, которые не имеют общих методов.

Например, у нас есть такой код:

type Numbers = readonly [1, 2, 3];
type Val = { value: Numbers | string };
// то есть, значением может быть, числа 1, 2, 3, либо строка

const myVal: Val = { value: 'a' };

Допустим, если у нас строка, то мы должны привести её к заглавным (большим) буквам. Если использовать такой код без Satisfies, будет ошибка:

myVal.value.toUpperCase()
// ^ Property 'toUpperCase' does not exist on type 'Numbers'.

Но, если использовать Satisfies, то всё будет в порядке:

const myVal = { value: 'a' } satisfies {value: string};
myVal.value.toUpperCase() // A

Utility types

Советую их изучить и применять на практике, порой они довольно сильно облегчают жизнь. Подробнее о них можно прочитать тут.

Вместо Function

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

F extends (...args: unknown[]) => unknown 
// где unknown заменить на те типы, которые вы используете в функции

К слову, тут же может пригодиться и infer для получения аргументов и возвращаемого значения:

const func = (a: number, b: number): string => 'Hello'
// для примера, функция с аргументами a и b, которая выводит Hello

type GetArgs<F> = F extends (...args: infer A) => unknown ? A : never
GetArgs<typeof func> // аргументы - [a: number, b: number]
  
type getReturnType<F> = F extends (...args: never[]) => infer R ? R : never
GetReturnType<typeof func> // string

Лучше юнионы

Иногда можно встретить подобный код:

interface User {
  loginData: "login" | "username";
  getLogin(): void;
  getUsername(): void;
}

Этот код плох тем, что можно использовать username, но при этом обращаться к getLogin() и наоборот. Чтобы этого не допустить, лучше использовать юнионы:

interface UserWithLogin {
  loginData: "login";
  getLogin(): void;
}
 
interface UserWithUsername {
  loginData: "username";
  getUsername(): void;
}
 
type User = UserWithLogin | UserWithUsername;

Юнионы итерабельны, то есть их можно использовать для прохождения циклической проверки:

type Numbers = 1 | 2 | 3
type OnlyRuKeys = {[R in Numbers]: boolean}
// {1: boolean, 2: boolean, 3: boolean}

Вместо заключения

Надеюсь, теперь вы будете лучше разбираться с Typescript и избавитесь от бесконечных any в проекте. Добавляем статью в закладки и не забываем использовать :)

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


  1. Alexandroppolus
    22.11.2023 06:55
    +2

    type Languages = {ru: boolean} | {en: boolean}
    // теперь нельзя использовать ru и en вместе

    Можно. Чтобы гарантировать ровно одно из двух полей, надо "запретить" второе:

    type Languages = {ru: boolean, en?: never} | {ru?: never, en: boolean};

    Так же тут следует упомянуть discriminated unions


    1. danila_egorenko Автор
      22.11.2023 06:55

      Спасибо за замечание, изменил пример, чтобы не вводить в заблуждение


  1. iliazeus
    22.11.2023 06:55

    К слову, тут же может пригодиться и infer для получения аргументов и возвращаемого значения:

    Тут лучше подойдут стандартные Parameters и ReturnType.


    1. danila_egorenko Автор
      22.11.2023 06:55

      Полностью согласен, это для примера, что с infer и функциями можно создавать довольно сложные типы


  1. iliazeus
    22.11.2023 06:55
    +3

    as const

    Допустим, у вас есть массив, который приходит с бекенда

    Пример неудачный - as const можно использовать только для compile-time констант, для данных с бекенда он ничем не поможет.


  1. insecto
    22.11.2023 06:55
    +2

    Используйте их везде, где только возможно.

    И превратите свой код в невыносимую перловку из угловых скобочек вопросиков и неверов.


    1. Alexandroppolus
      22.11.2023 06:55
      +2

      Да, правильнее было бы сказать - используйте только при необходимости. Пример:

      // ненужный генерик
      function getLen<T extends {length: number}>(x: T): number {
          return x.length;
      }
      
      // нормальный вариант без излишств
      function getLen(x: {length: number}): number {
          return x.length;
      }

      Советы из TFM по генерикам - тут


  1. Zukomux
    22.11.2023 06:55

    Используйте readonly по умолчанию, это позволит избежать случайного перезаписывания типов в вашем интерфейсе

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

    И стоит добавить сюда описание что такое type и что такое interface. В чем между ними основная разница и что предпочтительнее использовать. К сожалению, последнее мало кто понимает.