Хабрахабр

[Перевод] 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):

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

Резюме

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

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

Бонус

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

Теги
Показать больше

Похожие статьи

Добавить комментарий

Ваш e-mail не будет опубликован. Обязательные поля помечены *

Кнопка «Наверх»
Закрыть