- PVSM.RU - https://www.pvsm.ru -

5 заповедей TypeScript-разработчика

image

Всё больше и больше проектов и команд используют TypeScript. Однако просто применять TypeScript и выжимать из него максимум пользы — это очень разные вещи.

Представляю вам список высокоуровневых передовых практик использования TypeScript, которые помогут получить максимум преимуществ от применения этого языка.

Не лгите

Типы — это контракт. Что это значит? Когда вы реализуете функцию, её тип становится обещанием, данным другим разработчикам (или вам же самим в будущем!), что, будучи вызвана, эта функция вернет определенный тип значения.

В следующем примере тип функции getUser гарантирует, что она возвращает объект, у которого всегда есть два свойства: name и age.

interface User {
  name: string;
  age: number;
}

function getUser(id: number): User { /* ... */ }

TypeScript — очень гибкий язык. В нем много компромиссов, призванных облегчить внедрение языка. Например, он позволяет вам реализовать функцию getUser вот так:

function getUser(id: number): User {
  return { age: 12 } as User;
}

Не делайте так! Это ЛОЖЬ. Создавая такой код, вы ЛЖЕТЕ другим разработчикам (которые будут использовать вашу функцию в своих функциях). Они ожидают, что у объекта, возвращаемого функцией getUser, всегда будет какое-то поле name. Но его нет! Далее, что произойдет, когда ваш коллега напишет getUser(1).name.toString()? Вы прекрасно знаете, что…

Здесь, конечно, ложь выглядит очевидной. Однако, работая с большой базой кода, вы будете часто оказываться в ситуациях, когда значение, которое вы хотите вернуть (или передать), почти совпадает с ожидаемым типом. Чтобы найти причину несовпадения типов, нужны время и усилия, а вы торопитесь… поэтому вы решаете использовать приведение типов.

Однако, делая это, вы нарушаете священный контракт. ВСЕГДА лучше выделить время и понять, почему типы не совпадают, чем использовать приведение типов. Очень вероятно, что под поверхностью скрывается какой-нибудь баг времени выполнения.

Не лгите. Соблюдайте свои контракты.

Будьте точны

Типы — это документация. Документируя функцию, разве вы не хотите донести как можно больше информации?

// Возвращает объект
function getUser(id) { /* ... */ }

// Возвращает объект с двумя свойствами: name и age
function getUser(id) { /* ... */ }

// Если id является числом и пользователь с данным id существует,
// возвращает объект с двумя свойствами: name и age.
// В противном случае возвращает undefined.
function getUser(id) { /* ... */ }

Какой комментарий для функции getUser вам бы больше понравился? Чем больше вы знаете о том, что возвращает функция, тем лучше. Например, зная, что она может вернуть undefined, вы можете написать блок if для проверки того, определен ли объект, который вернула функция, — перед тем, как запрашивать свойства этого объекта.

Ровно то же самое и с типами: чем более точно описан тип, тем больше информации он передает.

function getUserType(id: number): string { /* ... */ }

function getUserType(id: number): 'standard' | 'premium' | 'admin' { /* ... */ }

Вторая версия функции getUserType гораздо более информативна, и поэтому вызывающий ее находится в гораздо более удобной ситуации. Обрабатывать величину легче, если вы наверняка знаете (контракты, помните?), что это будет одна из трех заданных строк, а не просто любая строка. Начать с того, что вы точно знаете — величина не может быть пустой строкой.

Рассмотрим более реальный пример. Тип State описывает состояние компонента, который запрашивает некоторые данные с бекэнда. Точен ли этот тип?

interface State {
  isLoading: boolean;
  data?: string[];
  errorMessage?: string;
}

Клиент, использующий данный тип, должен обрабатывать некоторые маловероятные сочетания значений свойств состояния. Например, невозможна ситуация, когда будут одновременно определены свойства data и errorMessage: запрос данных может либо быть успешным, либо завершиться с ошибкой.

Мы можем сделать тип намного более точным с помощью разграничивающих объединяющих типов (discriminated union types [1]):

type State =
   | { status: 'loading' }
   | { status: 'successful', data: string[] }
   | { status: 'failed', errorMessage: string };

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

Будьте точны. Передавайте в своих типах как можно больше информации.

Начинайте с типов

Так как типы являются одновременно и контрактом, и документацией, они отлично подходят для проектирования ваших функций (или методов).

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

Типы чрезвычайно полезны в этом процессе. Обдумывание приводит к созданию сигнатур типов функций, связанных с решением вашей задачи. И это замечательно, потому что вы фокусируетесь на том, что делают ваши функции, вместо раздумий о том, как они это делают.

В React JS есть понятие компонента высшего порядка (Higher Order Components, HOC). Это функции, которые каким-либо образом расширяют заданный компонент. К примеру, вы можете создать компонент высшего порядка withLoadingIndicator, который добавляет индикатор загрузки в существующий компонент.

Давайте напишем сигнатуру типа для этой функции. Функция принимает на вход компонент и возвращает тоже компонент. Для представления компонента мы можем воспользоваться типом React ComponentType.

ComponentType является обобщенным типом (generic type), который параметризуется типом свойств компонента. withLoadingIndicator принимает компонент и возвращает новый компонент, который отображает либо оригинальный компонент, либо индикатор загрузки. Решение о том, что именно отобразить, принимается исходя из значения нового логического свойства — isLoading. Таким образом, возвращаемому компоненту необходимы те же свойства, что и оригинальному, добавляется лишь новое свойство isLoading.

Окончательно оформим тип. withLoadingIndicator принимает компонент типа ComponentType<P>, где P обозначает тип свойств. withLoadingIndicator возвращает компонент с расширенными свойствами типа P & { isLoading: boolean }.

const withLoadingIndicator = <P>(Component: ComponentType<P>) 
    : ComponentType<P & { isLoading: boolean }> =>
        ({ isLoading, ...props }) => { /* ... */ }

Разбираясь с типами функции, мы были вынуждены думать о том, что будет у нее на входе и что на выходе. Другими словами, нам пришлось проектировать функцию. Написать её реализацию теперь — проще простого.

Начинайте с типов. Пусть типы вынуждают вас сначала проектировать, и лишь после этого писать реализацию.

Примите строгость

Первые три заповеди требуют от вас уделять особое внимание типам. К счастью, решая эту задачу, вы не обязаны делать все самостоятельно — зачастую сам компилятор TypeScript даст вам знать, когда ваши типы лгут или когда они недостаточно точны.

Можно помочь компилятору выполнять эту работу еще лучше, включив флаг --strict. Это мета-флаг, который подключает все опции строгой проверки типов: --noImplicitAny, --noImplicitThis, --alwaysStrict, --strictBindCallApply, --strictNullChecks, --strictFunctionTypes и --strictPropertyInitialization.

Что делают это флаги? Говоря в общем, их включение приводит к увеличению количества ошибок компиляции TypeScript. И это хорошо! Больше ошибок компиляции — больше помощи от компилятора.

Посмотрим, как включение флага --strictNullChecks помогает выявить ложь в коде.

function getUser(id: number): User {
    if (id >= 0) {
        return { name: 'John', age: 12 };
    } else {
        return undefined;
    }
}

Тип getUser гарантирует, что функция всегда возвращает объект типа User. Однако посмотрите на реализацию: функция может также вернуть значение undefined!

К счастью, включение флага --strictNullChecks приводит к ошибке компиляции:

Type 'undefined' is not assignable to type 'User'.

Компилятор TypeScript обнаруживает ложь. Чтобы избавиться от этой ошибки, просто честно скажите всю правду:

function getUser(id: number): User | undefined { /* ... */ }

Примите строгость проверки типов. Пусть компилятор оберегает вас от ошибок.

Будьте в курсе

Язык TypeScript развивается очень быстрыми темпами. Новый релиз выходит каждые два месяца. Каждый релиз привносит значительные улучшения языка и новые возможности.

Часто бывает так, что новые возможности языка позволяют определить типы точнее и проверять их строже.

Например, в версии 2.0 были представлены Discriminated Union Types (я упомянул их в заповеди Будьте точны).

Версия 3.2 представила флаг компилятора --strictBindCallApply, который включает корректную типизацию для функций bind, call и apply.

Версия 3.4 улучшила выведение типов (type inference) в функциях высшего порядка [2], что облегчило использование точных типов при написании кода в функциональном стиле.

Моя позиция такова, что знакомство с возможностями языка, вводимыми в последних версиях TypeScript, на самом деле стоит того. Часто это может помочь вам следовать остальным четырем заповедям из списка.

Хорошей стартовой точкой является официальная дорожная карта TypeScript [3]. Также будет неплохо регулярно проверять раздел TypeScript в Microsoft Devblog [4], так как все анонсы релизов выходят именно там.

Будьте в курсе новых возможностей языка, и пусть это знание работает на вас.

Резюме

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

Буду рад увидеть ваши мысли на этот счет в комментариях.

Бонус

Понравилась эта статья о TypeScript? Уверен, вам также понравится и этот бесплатный PDF: 10 ошибок разработки на TypeScript, которые делают ваш код небезопасным [5]

Автор: bevalorous

Источник [6]


Сайт-источник PVSM.RU: https://www.pvsm.ru

Путь до страницы источника: https://www.pvsm.ru/sovershenny-j-kod/325258

Ссылки в тексте:

[1] discriminated union types: https://www.typescriptlang.org/docs/handbook/advanced-types.html#discriminated-unions

[2] Версия 3.4 улучшила выведение типов (type inference) в функциях высшего порядка: https://codewithstyle.info/TypeScript-3-4-hidden-gem-propagated-generic-type-arguments/

[3] официальная дорожная карта TypeScript: https://github.com/Microsoft/TypeScript/wiki/Roadmap

[4] раздел TypeScript в Microsoft Devblog: https://devblogs.microsoft.com/typescript/

[5] 10 ошибок разработки на TypeScript, которые делают ваш код небезопасным: https://typescriptmasterclass.com/

[6] Источник: https://habr.com/ru/post/461565/?utm_source=habrahabr&utm_medium=rss&utm_campaign=461565