Использование Typescript с React – руководство для новичков

Автор оригинала: Ben McMahen
  • Перевод
Друзья, в преддверии выходных хотим поделиться с вами еще одной интересной публикацией, которую хотим приурочить к запуску новой группы по курсу «Разработчик JavaScript».



Потратив последние несколько месяцев на разработку приложений на React и библиотек с использованием Typescript, я решил поделиться некоторыми вещами, которые узнал за это время. В этом руководстве я расскажу вам про шаблоны, которые я использую для Typescript и React в 80% случаев.

Стоит ли изучать Typescript для разработки приложений на React? Стоит, еще как стоит! Для себя я осознал на практике, что строгая типизация приводит к написанию гораздо более надежного кода, быстрой разработке, особенно в крупных проектах. Сначала вы, вероятно, будете разочарованы, но по мере работы вы обнаружите, что хотя бы минимальный шаблон действительно будет очень кстати.

И если вы застряли на чем-то, помните, что вы всегда можете типизировать что- нибудь как any. Any – ваш новый друг. А теперь перейдем непосредственно к примерам.

Ваш базовый компонент react с typescript


Как же выглядит стандартный компонент react на typescript? Давайте сравним его с компонентом react в javascript.

import React from 'react'
import PropTypes from 'prop-types'

export function StandardComponent({ children, title = 'Dr.' }) {
  return (
    <div>
      {title}: {children}
    </div>
  )
}

StandardComponent.propTypes = {
  title: PropTypes.string,
  children: PropTypes.node.isRequired,
}

А теперь версия на typescript:

import * as React from 'react'

export interface StandardComponentProps {
  title?: string
  children: React.ReactNode
}

export function StandardComponent({
  children,
  title = 'Dr.',
}: StandardComponentProps) {
  return (
    <div>
      {title}: {children}
    </div>
  )
}

Очень похоже, не так ли? Мы заменили propTypes на интерфейс typescript.

Заголовок prop остается необязательным, в то время как все еще требуется prop наследника. Мы экспортировали наш интерфейс на случай, если другому компоненту понадобится на него ссылка.

Расширение стандартных атрибутов HTML


Если мы хотим, чтобы родительский компонент мог обеспечивать дополнительные типизированные атрибуты div, такие как aria-hidden, style или className, мы можем определить их в interface или же расширить встроенный интерфейс. В приведенном ниже примере, мы говорим, что наш компонент принимает любые стандартные свойства div в дополнение к заголовку и наследникам.

import * as React from 'react'

export interface SpreadingExampleProps
  extends React.HTMLAttributes<HTMLDivElement> {
  title?: string
  children: React.ReactNode
}

export function SpreadingExample({
  children,
  title = 'Dr.',
  ...other
}: SpreadingExampleProps) {
  return (
    <div {...other}>
      {title}: {children}
    </div>
  )
}

Обработка событий


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

export interface EventHandlerProps {
  onClick: (e: React.MouseEvent) => void
}

export function EventHandler({ onClick }: EventHandlerProps) {
  // handle focus events in a separate function
  function onFocus(e: React.FocusEvent) {
    console.log('Focused!', e.currentTarget)
  }

  return (
    <button
      onClick={onClick}
      onFocus={onFocus}
      onKeyDown={e => {
        // When using an inline function, the appropriate argument signature
        // is provided for us
      }}
    >
      Click me!
    </button>
  )
}

Не знаете, какую сигнатуру аргумента использовать? В редакторе наведите курсором на соответствующее свойство обработчика событий.

Использование дженериков с компонентами react


Это более продвинутая функция, но она действительно мощная. Как правило, вы определяете типы данных в компонентах react конкретными атрибутами. Предположим, вашему компоненту требуется объект profile.

interface ProfileType {
  name: string
  image: string
  age: number | null
}

interface ProfilesProps {
  profiles: Array<ProfileType>
}

function Profiles(props: ProfilesProps) {
  // render a set of profiles
}

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

Мы реализуем это так:

interface GenericsExampleProps<T> {
  children: (item: T) => React.ReactNode
  items: Array<T>
}

export function GenericsExample<T>({
  items,
  children,
}: GenericsExampleProps<T>) {
  return (
    <div>
      {items.map(item => {
        return children(item)
      })}
    </div>
  )
}

Немного странный пример… тем не менее он демонстрирует суть. Компонент принимает массив элементов любого типа, проходит по нему и вызывает функцию children как рендер функцию с элементом массива. Когда наш родительский компонент предоставляет колбэк рендера как наследника, элемент будет типизирован правильно!

Не поняли? Это нормально. Я сам еще не разобрался с дженериками до конца, но вам вряд ли понадобится понимать их досконально. Однако, чем больше вы будете работать с typescript, тем больше в этом будет смысла.

Типизация хуков (hooks)


Хуки в основном работают из коробки. Двумя исключениями могут быть только useRef и useReducer. Пример ниже показывает, как мы можем типизировать ссылки (refs).

import * as React from 'react'

interface HooksExampleProps {}

export function HooksExample(props: HooksExampleProps) {
  const [count, setCount] = React.useState(0)
  const ref = React.useRef<HTMLDivElement | null>(null)

  // start our timer
  React.useEffect(
    () => {
      const timer = setInterval(() => {
        setCount(count + 1)
      }, 1000)

      return () => clearTimeout(timer)
    },
    [count]
  )

  // measure our element
  React.useEffect(
    () => {
      if (ref.current) {
        console.log(ref.current.getBoundingClientRect())
      }
    },
    [ref]
  )

  return <div ref={ref}>Count: {count}</div>
}

Наше состояние автоматически типизируется, но мы вручную типизировали ref, чтобы показать, что он будет иметь значение null или содержать элемент div. Когда мы обращаемся к ref в функции useEffect, нам нужно убедиться, что он не равен null.

Типизация редуктора


С редуктором немного сложнее, но если он правильно типизирован, то это замечательно.

// Yeah, I don't understand this either. But it gives us nice typing
// for our reducer actions.
type Action<K, V = void> = V extends void ? { type: K } : { type: K } & V

// our search response type
interface Response {
  id: number
  title: string
}

// reducer actions. These are what you'll "dispatch"
export type ActionType =
  | Action<'QUERY', { value: string }>
  | Action<'SEARCH', { value: Array<Response> }>

// the form that our reducer state takes
interface StateType {
  searchResponse: Array<Response>
  query: string
}

// our default state
const initialState: StateType = {
  searchResponse: [],
  query: '',
}

// the actual reducer
function reducer(state: StateType, action: ActionType) {
  switch (action.type) {
    case 'QUERY':
      return {
        ...state,
        query: action.value,
      }

    case 'SEARCH':
      return {
        ...state,
        searchResponse: action.value,
      }
  }
}

interface ReducerExampleProps {
  query: string
}

export function ReducerExample({ query }: ReducerExampleProps) {
  const [state, dispatch] = React.useReducer(reducer, initialState)

  React.useEffect(
    () => {
      if (query) {
        // emulate async query
        setTimeout(() => {
          dispatch({
            type: 'SEARCH',
            value: [{ id: 1, title: 'Hello world' }],
          })
        }, 1000)
      }
    },
    [query]
  )

  return state.searchResponse.map(response => (
    <div key={response.id}>{response.title}</div>
  ))
}

Использование typeof и keyof чтобы типизировать варианты компонента


Предположим, что нам нужна кнопка, которая может иметь различный внешний вид, каждый из которых определен в объекте с набором ключей и стилей, например:

const styles = {
  primary: {
    color: 'blue',
  },
  danger: {
    color: 'red',
  },
}

Наш компонент кнопки должен принимать свойство type, которое может быть
любым ключом из объекта styles (например, «primary» или «danger»). Мы можем типизировать его достаточно просто:

const styles = {
  primary: {
    color: 'blue',
  },
  danger: {
    color: 'red',
  },
}

// creates a reusable type from the styles object
type StylesType = typeof styles

// ButtonType = any key in styles
export type ButtonType = keyof StylesType

interface ButtonProps {
  type: ButtonType
}

export function Button({ type = 'primary' }: ButtonProps) {
  return <button style={styles[type]}>My styled button</button>
}

Эти примеры помогут вам пройти 80% пути. Если вы застряли, то очень часто стоит
просто взглянуть на существующие примеры с открытым исходным кодом.

Sancho UI — это набор компонентов react,
построенный с помощью typescript и emotion.
Blueprint — это еще один набор компонентов
react, построенный на typescript.

Ну и по устоявшейся традиции ждем ваши комментарии.
  • +25
  • 24,9k
  • 7
OTUS. Онлайн-образование
507,69
Цифровые навыки от ведущих экспертов
Поделиться публикацией

Комментарии 7

    +2

    // Yeah, I don't understand this either. But it gives us nice typing
    // for our reducer actions.


    На месте автора оригинального текста я бы постеснялся публиковать такой Voodoo код. А совет с использованием any вообще сводит на нет смысл использования TypeScript.


    В качестве примера проекта можно добавить Formik

      +2

      Совет использовать ANY в сложных ситуациях на порядки ускоряет переход на TS. А может и вообще позволяет его провести.

        +2

        Ну вы скажете. В команде я один могу писать на TS, и пишу на нем какие-то ключевые части приложения, где без строгой типизации я вообще не представляю как обойтись. И я никогда не против, если JS-онли разработчик немного подправит ядро через any, а позже я сделаю рефакторинг и прочее, а затем чуваку ещё и дифф скину, типа, смотри как оно на тайпе выглядит, чтобы учился.
        Когда выразительности JS уже недостаточно для работы, тогда ты с лёгкостью учишь TS, благо для любого среднего программиста это неделя. А начинающему разработчику никогда не объяснить, почему строгие типы — это классно, сам же был таким, и пальцем у виска крутил, когда первые версии TS видел, однако с тех пор и TS серьезно развился.

        +2
        type Action<K, V = void> = V extends void ? { type: K } : { type: K } & V

        Когда в руках молоток, то все кажется гвоздями… Зачем тут вообще нужен условный тип? Вот так не проще ли сделать?


        type Action<K, V = {}> = { type: K } & V

        Но еще лучше взять решение вот отсюда: https://habr.com/ru/company/alfa/blog/452620/

          –2
          import * as React from 'react'
          А потом у нас лендинги по 3 секунды загружаются.
            0
            Примеры реализаций привел. Понимаю тебя, это просто перевод, но если пишешь код на react.js мог бы привести и примеры применения. Хочется взять статью почитать и реализовать, а не искать дополнительно, а как это сделать и куда это всучить, чтобы заработало. Приведи примеры применения или может кто-нибудь из читателей привести.

            Только полноправные пользователи могут оставлять комментарии. Войдите, пожалуйста.

            Самое читаемое