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

в 13:59, , рубрики: javascript, React, ReactJS, TypeScript, Блог компании OTUS. Онлайн-образование

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

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

Потратив последние несколько месяцев на разработку приложений на 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.

Ну и по устоявшейся традиции ждем ваши комментарии.

Автор: MaxRokatansky

Источник

* - обязательные к заполнению поля


https://ajax.googleapis.com/ajax/libs/jquery/3.4.1/jquery.min.js