[Перевод] 5 заповедей TypeScript-разработчика
Всё больше и больше проектов и команд используют 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
.
ComponentType
является обобщенным типом (generic type), который параметризуется типом свойств компонента. withLoadingIndicator
принимает компонент и возвращает новый компонент, который отображает либо оригинальный компонент, либо индикатор загрузки. Решение о том, что именно отобразить, принимается исходя из значения нового логического свойства — isLoading
. Таким образом, возвращаемому компоненту необходимы те же свойства, что и оригинальному, добавляется лишь новое свойство isLoading
.
Окончательно оформим тип. withLoadingIndicator
принимает компонент типа ComponentType
, где P
обозначает тип свойств. withLoadingIndicator
возвращает компонент с расширенными свойствами типа P & { isLoading: boolean }
.
const withLoadingIndicator = (Component: ComponentType
)
: ComponentType
=>
({ 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) в функциях высшего порядка, что облегчило использование точных типов при написании кода в функциональном стиле.
Моя позиция такова, что знакомство с возможностями языка, вводимыми в последних версиях TypeScript, на самом деле стоит того. Часто это может помочь вам следовать остальным четырем заповедям из списка.
Хорошей стартовой точкой является официальная дорожная карта TypeScript. Также будет неплохо регулярно проверять раздел TypeScript в Microsoft Devblog, так как все анонсы релизов выходят именно там.
Будьте в курсе новых возможностей языка, и пусть это знание работает на вас.
Резюме
Надеюсь, изложенный список показался вам полезным. Как всегда и во всем, не стоит слепо следовать этим заповедям. Но я твердо убежден, что эти правила сделают вас более хорошим TypeScript-разработчиком.
Буду рад увидеть ваши мысли на этот счет в комментариях.
Бонус
Понравилась эта статья о TypeScript? Уверен, вам также понравится и этот бесплатный PDF: 10 ошибок разработки на TypeScript, которые делают ваш код небезопасным