Расширенный TypeScript с реальным примером (часть 1)
Опубликовано: 2020-11-06На прошлой неделе мы увидели небольшое введение в TypeScript и, в частности, рассказали о том, как этот язык, расширяющий JavaScript, может помочь нам создавать более надежный код. Поскольку это было только введение, я не говорил о некоторых функциях TypeScript, которые вы, возможно, захотите (и, возможно, захотите) использовать в своих проектах.
Сегодня я научу вас профессионально применять TypeScript в реальном проекте. Для этого мы начнем с просмотра части исходного кода Nelio Content, чтобы понять, с чего мы начинаем и какие ограничения у нас есть в настоящее время. Далее мы будем постепенно улучшать исходный код JavaScript, добавляя небольшие дополнительные улучшения, пока не получим полностью типизированный код.
Использование исходного кода Nelio Content в качестве основы
Как вы, возможно, уже знаете, Nelio Content — это плагин, который позволяет вам делиться контентом вашего сайта в социальных сетях. В дополнение к этому, он также включает в себя несколько функций, которые призваны помочь вам постоянно создавать более качественный контент в своем блоге, например, качественный анализ ваших сообщений, редакционный календарь для отслеживания предстоящего контента, который вам нужно написать, и так далее. .

В прошлом месяце мы опубликовали версию 2.0, полностью переработанную как визуально, так и внутренне наш плагин. Мы создали эту версию, используя все новые технологии, доступные сегодня в WordPress (о чем мы недавно говорили в нашем блоге), включая интерфейс React и магазин Redux.
Итак, в сегодняшнем примере мы будем улучшать последний. То есть мы увидим, как мы можем набрать Redux store.
Селекторы редакционного календаря Nelio Content
Редакционный календарь — это пользовательский интерфейс, который показывает записи блога, которые мы запланировали на каждый день недели. Это означает, что нашему хранилищу Redux потребуются как минимум две операции запроса: одна сообщает нам сообщения, которые запланированы на любой день, а другая, учитывая идентификатор сообщения, возвращает все его атрибуты.
Предполагая, что вы читали наши сообщения на эту тему, вы уже знаете, что селектор в Redux получает в качестве своего первого параметра состояние со всей информацией о нашем приложении, за которым следуют любые дополнительные параметры, которые могут ему понадобиться. Таким образом, наши два примера селекторов в JavaScript будут выглядеть примерно так:
function getPost( state, id ) { return state.posts[ id ]; } function getPostsInDay( state, day ) { return state.days[ day ] ?? []; } Если вам интересно, откуда я знаю, что состояние имеет атрибуты posts и days , то это довольно просто: потому что я их определил. Но вот почему я решил реализовать их так.
Мы знаем, что хотим иметь доступ к нашей информации с двух разных точек зрения: публикации за день или публикации по идентификатору. Поэтому кажется, что имеет смысл разделить наши данные на две части:
- С одной стороны, у нас есть атрибут
posts, в котором мы перечислили все сообщения, которые мы получили с сервера и сохранили в нашем магазине Redux. По логике, мы могли бы сохранить их в массив и сделать последовательный поиск поста, чей ID совпадает с ожидаемым… но объект ведет себя как словарь, предлагая более быстрый поиск. - С другой стороны, нам также нужен доступ к сообщениям, запланированным на определенный день. Опять же, мы могли бы использовать только один массив для хранения всех сообщений и фильтровать его, чтобы найти сообщения, относящиеся к определенному дню, но наличие еще одного словаря предлагает более быстрое решение для поиска.
Действия и редукторы в Nelio Content
Наконец, если нам нужен динамический календарь, мы должны реализовать функции, позволяющие нам обновлять информацию, хранящуюся в нашем магазине. Для простоты мы собираемся предложить два простых метода: один позволяет нам добавлять новые сообщения в календарь, а другой позволяет нам изменять атрибуты существующих.
Обновления в магазине Redux требуют двух частей. С одной стороны, у нас есть действия, сигнализирующие об изменении, которое мы хотим внести, а с другой — редуктор, который, учитывая текущее состояние нашего хранилища и действие, запрашивающее обновление, применяет необходимые изменения к текущему состоянию для создать новое состояние.
Итак, принимая это во внимание, в нашем магазине могут быть следующие действия:
function receiveNewPost( post ) { return { type: 'RECEIVE_NEW_POST', post, }; } function updatePost( postId, attributes ) { return { type: 'UPDATE_POST', postId, attributes, } }а вот редуктор:
function reducer( state, action ) { state = state ?? { posts: {}, days: {} }; const postIds = Object.keys( state.posts ); switch ( action.type ) { case 'RECEIVE_NEW_POST'; if ( postIds.includes( action.postId ) ) { return state; } return { posts: { ...state.posts, [ action.post.id ]: action.post, }, days: { ...state.days, [ action.post.day ]: [ ...state.days[ action.post.day ], action.post.id, ], }, }; case 'UPDATE_POST'; if ( ! postIds.includes( action.postId ) ) { return state; } const post = { ...state.posts[ action.postId ], ...action.attributes, }; return { posts: { ...state.posts, [ post.id ]: post, }, days: { ...Object.keys( state.days ).reduce( ( acc, day ) => ( { ...acc, [ day ]: state.days[ day ].filter( ( postId ) => postId !== post.id ), } ), {} ), [ post.day ]: [ ...state.days[ post.day ], post.id, ], }, }; } return state; }Не торопитесь, чтобы понять все это, и давайте двигаться вперед!
От JavaScript к TypeScript
Первое, что мы должны сделать, это перевести предыдущий код на TypeScript. Что ж, поскольку TypeScript является надмножеством JavaScript, он уже есть… но если вы скопируете и вставите предыдущие функции в TypeScript Playground, вы увидите, что компилятор довольно сильно ругается из-за слишком большого количества переменных с неявным типом any . Итак, давайте сначала исправим это, явно добавив некоторые базовые типы.
Все, что нам нужно сделать, это явно добавить тип any ко всему, что является «сложным» (например, к состоянию нашего приложения), и использовать number , string или что-то еще, что мы хотим, для любой другой переменной/аргумента. Например, исходный селектор JavaScript:
function getPost( state, id ) { return state.posts[ id ]; }с явными типами TypeScript будет выглядеть так:
function getPost( state: any, id: number ): any | undefined { return state.posts[ id ]; } Как видите, простое действие по вводу нашего кода (даже когда мы используем «универсальные типы») предлагает много информации с первого взгляда; явное улучшение по сравнению с базовым JavaScript! В этом случае, например, мы видим, что getPost ожидает number (идентификатор сообщения — это целое число, помните?), и результатом будет либо что-то, если сообщение существует ( any ), либо ничего, если оно не существует ( undefined ).
Здесь у вас есть ссылка со всем типом кода, использующим простые типы, чтобы компилятор не жаловался.
Создание и использование пользовательских типов данных в TypeScript
Теперь, когда компилятор доволен нашим исходным кодом, пришло время немного подумать о том, как мы можем его улучшить. Для этого я всегда предлагаю начать с моделирования концепций, которые у нас есть в нашей области.

Создание пользовательского типа для сообщений
Мы знаем, что наш магазин будет в первую очередь содержать сообщения, поэтому я бы сказал, что первым шагом будет моделирование того, что представляет собой сообщение и какая информация о нем у нас есть. Мы уже видели, как создавать пользовательские типы на прошлой неделе, поэтому давайте попробуем сегодня с концепцией поста:
type Post = { id: number; title: string; author: string; day: string; status: string; isSticky: boolean; }; Здесь нет сюрпризов, верно? Post — это объект с несколькими атрибутами, такими как числовой id , текстовый title и т. д.
Еще одна важная часть информации, которую имеет любой магазин Redux, — это, как вы уже догадались, его состояние. В предыдущем разделе мы уже обсуждали его атрибуты, поэтому давайте определим базовую форму нашего типа State :
type State = { posts: any; days: any; }; Улучшение типа State
Теперь мы знаем, что у State есть два атрибута ( posts и days ), но мы мало что знаем об их значении, поскольку они могут быть any . Мы сказали, что хотим, чтобы оба атрибута были словарями. То есть, учитывая определенный запрос (либо идентификатор поста для posts , либо дату для days ), нам нужны связанные данные (пост или список постов соответственно). Мы знаем, что можем реализовать словарь с помощью объекта, но как нам представить словарь в TypeScript?
Если мы посмотрим на документацию по TypeScript, то увидим, что она включает в себя несколько типов утилит для решения довольно распространенных ситуаций. В частности, есть тип под названием Record , который кажется нам нужным: он позволяет нам вводить переменную, используя пары ключ/значение, в которых ключ имеет определенный тип Keys , а значения имеют тип Type . Если мы применим этот тип к нашему примеру, мы получим что-то вроде этого:
type State = { posts: Record<number, Post>; days: Record<string, number[]>; }; С точки зрения компилятора, тип Record работает таким образом, что при любом значении Keys (в нашем примере это number для posts и string для days ) его результатом всегда будет объект типа Type (в нашем случае Post или number[] соответственно). Проблема заключается не в том, как мы хотим, чтобы наш словарь вел себя: когда мы ищем определенный пост, используя его идентификатор, мы хотим, чтобы компилятор знал, что мы можем найти или не найти связанный пост, что означает, что результатом может быть либо Post или undefined .
К счастью, мы можем легко исправить это, используя еще один тип утилиты, тип Partial :
type State = { posts: Partial< Record<number, Post> >; days: Partial< Record<string, number[]> >; };Улучшение нашего кода с помощью псевдонимов типов
Взгляните на атрибут posts в нашем состоянии… Что вы видите? Словарь, который индексирует Post типа Post с номерами, верно? Теперь представьте, что вы просматриваете этот код на работе. Если вы столкнетесь с таким типом, вы можете предположить, что number проиндексированных сообщений, вероятно, является идентификатором проиндексированных сообщений… но это всего лишь предположение; вам придется просмотреть код, чтобы быть уверенным в этом. А как же days ? «Случайные строки, индексирующие списки чисел». Это не очень полезно, не так ли?
Типы TypeScript помогают нам писать более надежный код благодаря проверкам компилятора, но они предлагают гораздо больше. Если вы используете осмысленные типы, ваш код будет лучше документирован и его будет легче поддерживать. Итак, давайте назовем существующие типы псевдонимами, чтобы создать осмысленные типы, не так ли?
Например, зная, что идентификаторы сообщений ( number ) и даты ( string ) относятся к нашему домену, мы можем легко создать следующие псевдонимы типов:
type PostId = number; type Day = string;а затем переписать наши исходные типы, используя эти псевдонимы:
type Post = { id: PostId; title: string; author: string; day: Day; status: string; isSticky: boolean; }; type State = { posts: Partial< Record<PostId, Post> >; days: Partial< Record<Day, PostId[]> >; }; Другой псевдоним типа, который мы можем использовать для улучшения читаемости нашего кода, — это тип Dictionary , который «скрывает» сложность использования Partial и Record за удобной структурой:
type Dictionary<K extends string | number, T> = Partial< Record<K, T> >;сделать наш исходный код более понятным:
type State = { posts: Dictionary<PostId, Post>; days: Dictionary<Day, PostId[]>; }; Вот и все! Вот оно! Всего с тремя простыми псевдонимами типов мы смогли документировать код гораздо лучше, чем с помощью комментариев. Любой разработчик, который придет после нас, сможет с первого взгляда понять, что posts — это словарь, который индексирует объекты типов Post с помощью их PostId , а days — это структура данных, которая, учитывая Day , возвращает список идентификаторов сообщений. Это довольно круто, если вы спросите меня.
Но не только сами определения типов станут лучше… если мы будем использовать эти новые типы во всем нашем коде:
function getPost( state: State, id: PostId ): Post | undefined { return state.posts[ id ]; }он также выигрывает от этого нового семантического слоя! Вы можете увидеть новую версию нашего типизированного кода здесь.
Да, кстати, имейте в виду, что псевдонимы типов с точки зрения компилятора неотличимы от «исходного» типа. Это означает, что, например, PostId и number полностью взаимозаменяемы. Поэтому не ожидайте, что компилятор вызовет ошибку, если вы присвоите PostId number или наоборот (как вы можете видеть в этом небольшом примере); они просто служат для добавления семантики в наш исходный код.
Следующие шаги
Как видите, вы можете постепенно набирать код JavaScript, используя типы TypeScript, и при этом его качество и читабельность улучшаются. В сегодняшнем посте мы подробно рассмотрели пример реальной реализации приложения React + Redux и увидели, как его можно улучшить с относительно небольшими усилиями. Но нам еще предстоит пройти долгий путь.
В следующем посте мы напечатаем все оставшиеся переменные/аргументы, которые в настоящее время используют тип any , а также изучим некоторые продвинутые возможности TypeScript. Надеюсь, вам понравилась эта первая часть, и если да, то поделитесь ею с друзьями и коллегами.
Избранное изображение Даниэль Макиннес на Unsplash.
