Главная » Статьи » Шпаргалка по React с TypeScript

Шпаргалка по React с TypeScript

Шпаргалка по React с TypeScript

От автора: благодаря этому руководству вы узнаете о наиболее распространенных типах, которые могут понадобиться при разработке приложения React-TypeScript.

Как типизировать React props

Поскольку React props используются для отправки данных от одного компонента React в другой, существует множество типов, которые вы можете использовать для типизации props. Чтобы записать типы props, нужно добавить двоеточие и буквенное обозначение (: {}) рядом с деструктурирующим назначением дочерних props в объявлении компонента React. Вот пример типизации string и number:

const App = ({ title, score }: { title: string, score: number }) => ( <h1>{title} = {score}</h1>
)

Создание алиаса для props

Поскольку в React принято записывать один компонент в один файл .js или .jsx, вы можете объявить алиас type для props компонента, чтобы упростить чтение кода. Вот пример создания алиаса type для props компонента App:

type Props = { title: string, score: number
} const App = ({ title, score }: Props) => ( <h1>{title} = {score}</h1>
)

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

Типизация дополнительных props

Вы можете сделать props необязательными, добавив символ вопросительного знака «?» после имени props. В следующем примере props для title становятся необязательными:

type Props = { title?: string, score: number
}

Необязательность для props означает, что вы можете визуализировать компонент без передачи props, но когда вы передаете props, они должна быть объявленного типа.

Список типов для props компонента React

Теперь, когда вы знаете, как проверить тип props, вот список общих типов, которые вы можете использовать в своем приложении React. Во-первых, у вас есть примитивные типы, такие как string, number и boolean, как показано ниже:

type Props = { // primitive types title: string, score: number, isWinning: boolean
}

Вы также можете создать массив одного типа, добавив буквенное обозначение массива ([]) после типа следующим образом:

type Props = { title: string[], // an array of string score: number, isWinning: boolean
}

Также, вы можете указать значения, которые могут быть приняты для props. Вам нужно разделить литералы с помощью оператора вертикальной черты «|» как показано ниже:

type Props = { priority: "high" | "normal" | "low", score: 5 | 9 | 10
}

TypeScript выдаст статическую ошибку, если указанное выше значение priority или score не соответствует ни одному из литеральных значений. Также вы можете добавить объект в props следующим образом:

type Props = { user: { username: string, age: number, isMember: boolean }
}

Если у вас есть массив объектов, просто добавьте нотацию массива в конце объявления следующим образом:

type Props = { user: { username: string, age: number, isMember: boolean }[] // right here
}

React props также могут иметь такие функции, как onClick и onChange. Вы можете типизировать параметры, принимаемые функцией, или взять объект event из HTML, как показано ниже:

type Props = { // function that returns nothing onClick: () => void, // function accepts a parameter and has return type onChange: (target: string) => boolean, // function that takes an event handleSubmit: (event: React.FormEvent<HTMLFormElement>) => void
}

Если вы объявляете функцию onChange в теле компонента, вы можете сразу же проверить параметр и типы возвращаемых значений, как показано ниже:

const App = () => { const [message, setMessage] = useState("") const onChange = (e: React.FormEvent<HTMLInputElement>): void => { setMessage(e.currentTarget.value); } // code omitted for clarity..
}

Наконец, компоненты React могут принимать другой компонент в качестве дочерних props, поэтому вам нужно использовать ReactNode для типизации этих дочерних props:

type Props = { children: React.ReactNode
} const App = ({ children }: Props) => ( <div>{children}</div>
)

И это наиболее распространенные типы, которые вы можете использовать для React props. Давайте научимся типизировать функциональные компоненты React!

Как типизировать функциональные компоненты React

Библиотека TypeScript’s Definitely Typed включект React.FunctionComponent (или React.FC для краткости), которые можно использовать для типизации функциональных компонентов React.

Вы можете комбинировать type Props и тип React.FC, чтобы создать типобезопасный функциональный компонент props следующим образом:

type Props = { title: string
} const App: React.FC<Props> = ({title}) => { return ( <h1>{title}</h1> )
}

Когда вы вызываете компонент App, вам нужно будет указать message с типом string. Но поскольку TypeScript может определить тип вашей переменной, вы можете убрать типизацию компонента с помощью React.FC следующим образом:

type Props = { title: string
} const App = ({ title }: Props) => <div>{title}</div>
// App type will be inferred

Если у вас есть несколько props для компонента, вы даже можете типизировать props одной строкой, как показано ниже, избавившись от необходимости создавать тип Props:

const App = ({ title }: { title: string }) => <div>{title}</div>

Благодаря функции предполагаемого типа TypeScript вам вообще не нужно типизировать функциональные компоненты React.

Как типизировать хуки React

Хуки React поддерживаются библиотекой @types/react начиная с версии 16.8. Как правило, Typescript должен иметь возможность задать тип для ваших хуков, если у вас нет особых случаев, когда тип должен быть объявлен явно. Давайте посмотрим, как типизировать хуки React, и начнем с хука useState.

Типизация хука useState

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

const App = () => { const [title, setTitle] = useState("") // type is string const changeTitle = () => { setTitle(9) // error: number not assignable to string! }
}

Но когда вам нужно инициализировать состояние такими значениями, как null или undefined, тогда нужно добавить generic при инициализации состояния. Generic позволяет использовать несколько типов для хука useState, как показано ниже:

// title is string or null
const [title, setTitle] = useState<string | null>(null) // score is number or undefined
const [score, setScore] = useState<number | undefined>(undefined)

Когда у вас есть сложный объект в качестве значения состояния, вы можете создать interface или type для этого объекта следующим образом:

interface Member { username: string, age?: number
} const [member, setMember] = useState<Member | null>(null)

Вот так вы можете типизировать хуки useState в своем приложении.

Типизация хуков useEffect и useLayoutEffect

Вам не нужно типизировать хуки useEffect и useLayoutEffect, поскольку они не имеют возвращаемого значениями. Функция очистки для хука useEffect также не имеет значения, которое можно изменить. Вы можете писать эти хуки как обычно.

Типизация хука useContext

Тип хука useContext обычно определяется из начального значения, которое вы передали в функцию createContext() следующим образом:

const AppContext = createContext({ authenticated: true, lang: 'en', theme: 'dark'
}) const MyComponent = () => { const appContext = useContext(AppContext) //inferred as an object return <h1>The current app language is {appContext.lang}</h1>
}

Приведенное выше значение контекста будет выведено как следующий объект:

{ authenticated: boolean, lang: string, theme: string
}

В качестве альтернативы вы также можете создать type, который будет служить универсальным для возвращаемого значения CreateContext. Например, предположим, что у вас есть ThemeContext, который имеет только два значения: light и dark. Вот как вы типизируете контекст:

type Theme = 'light' | 'dark'
const ThemeContext = createContext<Theme>('dark')

Тип будет использоваться в вашем коде позже, когда вы установите значение контекста с помощью ThemeContext.Provider. Затем хук useContext определит тип из объекта контекста ThemeContext, который вы передали в качестве аргумента:

const App = () => { const theme = useContext(ThemeContext) return <div>The theme is {theme}</div>
}

Типизация хука useRef

Основываясь на документации React, хук useRef обычно используется для ссылки на input элемент HTML следующим образом:

function TextInputWithFocusButton() { const inputEl = useRef(null); const onButtonClick = () => { // `current` points to the mounted text input element inputEl.current.focus(); }; return ( <> <input ref={inputEl} type="text" /> <button onClick={onButtonClick}>Focus the input</button> </> );
}

Вы можете написать общий вариант использования, который принимает HTMLInputElement, как показано ниже:

const inputRef = useRef<HTMLInputElement>(null)

Вам не нужно добавлять null к типу дженерика, потому что HTMLInputElement принимает уже одно из двух значений: HTMLInputElement | null.

Типизация хука useMemo

Хук useMemo возвращает мемоизированное значение, поэтому тип будет определятся возвращенным значением:

const num = 24
// inferred as a number from the returned value below
const result = useMemo(() => Math.pow(10, num), [num])

Типизация хука useCallback

Хук useCallback возвращает мемоизированную функцию обратного вызова, поэтому тип будет определятся из значения, возвращаемого функцией обратного вызова:

const num = 9 const callbackFn = useCallback( (num: number) => { return num * 2 // type inferred as a number }, [num])

Типизация пользовательских хуков

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

function useFriendStatus(friendID: number) { const [isOnline, setIsOnline] = useState(false); // code for changing the isOnline state omitted.. return isOnline;
} const status = useFriendStatus(9) // inferred type boolean

Когда вы возвращаете массив, подобно как в useState, вам нужно указать возвращаемое значение как const, чтобы TypeScript не типизировал ваш тип как объединение:

function useCustomHook() { return ["Hello", false] as const }

Без указания const -TypeScript будет типизировать возвращаемые значения как (string | boolean) [] вместо [string, boolean]

Вот как можно типизировать хуки React. Давайте теперь узнаем, как типизировать HTML-события и формы.

Типизация HTML-событий и форм

TypeScript может правильно определить большинство типов событий HTML, поэтому вам не нужно явно указывать тип. Например, событие onClick элемента кнопки будет определено TypeScript как React.MouseEvent:

const App = () => ( <button onClick={ (e) => console.log("Clicked")}>button</button> // ^^^ e inferred as React.MouseEvent<HTMLButtonElement, MouseEvent>
)

Для HTML-форм вам нужно будет указать тип onSubmit как React.FormEvent, потому что интерфейс по умолчанию Any выдаст ошибку. Вот пример формы React в TypeScript:

const App = () => { const [email, setEmail] = useState("")const handleSubmit = (e: React.FormEvent) => { e.preventDefault() // handle submission here... alert(`email value: ${email}`) } return ( <form onSubmit={handleSubmit}> <div> <label> Email: <input type="email" name="email" onChange={(e) => setEmail(e.currentTarget.value)} // ^^^ onChange inferred as React.ChangeEvent /> </label> </div> <div> <input type="Submit" value="Submit" /> </div> </form> )
}

Понимание различных типизаций для компонентов React

Хотя TypeScript может определить тип возвращаемого значения функциональных компонентов React, у вас может быть проект с правилом линтинга, которое требует, чтобы тип возвращаемого значения был явно определен. Библиотека @types/react имеет несколько типов, которые вы можете использовать для определения типа возвращаемого значения функциональных компонентов React. Вот они: ReactElement, JSX.Element,ReactNode.

Этот раздел посвящен тому, чтобы помочь вам разобраться в этих типах и в том, когда их использовать.
ReactElement — это интерфейс для объекта с типом, props и ключевыми свойствами, как показано ниже:

type Key = string | numberinterface ReactElement<P = any, T extends string | JSXElementConstructor<any> = string | JSXElementConstructor<any>> { type: T; props: P; key: Key | null;
}

JSX.Element — это расширение ReactElement, в котором реализованы тип <T> и свойства <P>, как вы можете увидеть в репозитории:

declare global { namespace JSX { interface Element extends React.ReactElement<any, any> { } }
}

Тип для ReactElement более строгий, чем в JSX.Element, но по сути они такие же. Наконец, ReactNode — это очень свободный тип, поскольку он включает все, что может быть возвращено методом render() компонентов класса React. В репозитории ReactNode определяется так:

type ReactNode = ReactChild | ReactFragment | ReactPortal | boolean | null | undefined;

Вот почему, когда ваш компонент имеет дочерние props, которые может получать другой компонент, рекомендуется использовать ReactNode в качестве типа, потому что он может получать все, что может быть отображено с помощью React.

С другой стороны, ReactElement и JSX.Element более строги по сравнению с ReactNode, поскольку не позволяют возвращать такие значения, как null.

Когда использовать каждый тип?

Тип ReactNode лучше всего использовать для типизации дочерних props, которые могут получать другой компонент React или элементы JSX, например:

const App = ({ children }: { children: React.ReactNode }) => { return <div>{children}</div>
} // At index.tsx
<App> <Header/> <h2>Another title</h2>
</App>

Это связано с тем, что оба типа ReactElement и JSX.Element более строги к типу возвращаемого значения (не допускают null) и ожидают, что вы вернете один элемент.

Чтобы принять как один, так и несколько дочерних элементов для этих двух типов, вам необходимо использовать ReactElement | ReactElement [] или JSX.Element | JSX.Element [] как дочерний тип.

Типы ReactElement и JSX.Element больше подходят для явного определения типа возвращаемого значения компонента React следующим образом:

const App = () : React.ReactElement | JSX.Element => { return <div>hello</div>
}

Но поскольку мы говорим здесь о лучших практиках, я рекомендую вам следовать определению интерфейса FunctionComponent в библиотеке типов, в котором используется ReactElement <any, any> | null:

interface FunctionComponent<P = {}> { (props: PropsWithChildren<P>, context?: any): ReactElement<any, any> | null; propTypes?: WeakValidationMap<P> | undefined; contextTypes?: ValidationMap<any> | undefined; defaultProps?: Partial<P> | undefined; displayName?: string | undefined;
}

И поскольку JSX.Element в точности расширяет ReactElement <any, any>, вы можете определить тип возвращаемого значения функционального компонента React следующим образом:

const App = () : JSX.Element | null => { return <div>hello</div>
}

Таким образом, ваш компонент по-прежнему может ничего не отображать, возвращая null. Я надеюсь, что этот раздел помог вам понять различные типы, которые можно использовать для типизации компонентов React.

Как типизировать (расширять) HTML-элементы

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

Некоторые полезные компоненты, которые вы можете создать для своего приложения, — это button, img или input. Библиотека @types/response поставляется с типом ComponentPropsWithoutRef, который вы можете использовать для получения всех собственных атрибутов HTML-элемента в качестве типа для props вашего компонента.

Например, элемент button уже знает об собственном атрибуте onClick, но когда вы создаете компонент React <Button>, вам нужно определить props, используя интерфейс или такой тип:

type ButtonProps = { children: React.ReactNode onClick: () => void
} const Button = ({ children, onClick }: ButtonProps) => { return <button onClick={onClick}>{children}</button>
}

В приведенном выше примере вам нужно добавить еще один props в ButtonProps, как показано ниже:

type ButtonProps = { children: React.ReactNode onClick: () => void disabled: boolean type: 'button' | 'submit' | 'reset' | undefined }

Можно использовать тип ComponentPropsWithoutRef, так что вам не нужно добавлять эти собственные атрибуты HTML к типу по мере роста вашего приложения. Вы можете создать тип, который имеет все атрибуты button в виде таких свойств:

type ButtonProps = React.ComponentPropsWithoutRef<"button"> const Button = ({ children, onClick, type }: ButtonProps) => { return ( <button onClick={onClick} type={type}> {children} </button> )
}

Тип ComponentPropsWithoutRef <»button»> имеет все props элемента HTML button. Если вы хотите создать компонент <Img>, вы можете использовать тип ComponentPropsWithoutRef <»img»>:

type ImgProps = React.ComponentPropsWithoutRef<"img"> const Img = ({ src, loading }: ImgProps) => { return <img src={src} loading={loading} />
}

Вам нужно только изменить общий тип ComponentPropsWithoutRef <T>, чтобы расширить различные элементы HTML. Например:

ComponentPropsWithoutRef <’img’> для расширения элемента <img>

ComponentPropsWithoutRef <’button’> для расширения элемента <button>

ComponentPropsWithoutRef <’a’> для расширения элемента <a>

И так далее. Когда вам нужно добавить пользовательский prop, которого нет в HTML-элементе, вы можете создать интерфейс, расширяющий собственные атрибуты, следующим образом:

interface ImgProps extends React.ComponentPropsWithoutRef<"img"> { customProp: string;
} const Img = ({ src, loading, customProp }: ImgProps) => { // use the customProp here.. return <img src={src} loading={loading} />;
}

Это особенно полезно, если вам нужен специальный prop для определения внешнего вида вашего компонента. В следующем примере пользовательский prop color используется для определения атрибута CSS style:color элемента h1:

interface headerProps extends React.ComponentPropsWithoutRef<"h1"> { variant: "primary" | "secondary";
} const Header = ({ children, variant }: headerProps) => { return ( <h1 style={{color: variant === "primary" ? "black" : "red" }}> {children} </h1> );
};

Тип ComponentPropsWithoutRef упрощает создание компонента, который является расширением элементов HTML, без необходимости самостоятельно вводить все возможные параметры prop. Вы даже можете добавить дополнительные props, расширив интерфейс.

Интерфейс ComponentPropsWithoutRef также имеет двойника под названием ComponentPropsWithRef, который вы можете использовать, когда нужно перенаправить ссылку на дочерние элементы. Узнайте больше о переадресации ссылок здесь.

ComponentPropsWithoutRef против [Element] HTMLAttributes

Если вы раньше использовали TypeScript в React, возможно, вы знакомы с интерфейсом [Element] HTMLAttributes из библиотеки @types/react, который можно использовать для расширения HTML-элементов следующим образом:

type ButtonProps = React.ButtonHTMLAttributes<HTMLButtonElement> type ImgProps = React.ImgHTMLAttributes<HTMLImageElement>

Интерфейсы [Element]HTMLAttributes создают тот же тип, что и интерфейс ComponentPropsWithoutRef, но они более подробны, поскольку вам нужно использовать другой интерфейс и дженерик для каждого элемента HTML.

С другой стороны, ComponentPropsWithoutRef требует, чтобы вы изменили только общий тип <T>. Оба подходят для расширения HTML-элементов в компонентах React. Вы можете увидеть объяснение автора библиотеки здесь.

Когда использовать type и interface?

И type, и interface в TypeScript могут использоваться для определения свойств, компонентов и хуков React. Из справочника по TypeScript:

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

При использовании интерфейсов вы можете свободно расширять его следующим образом:

interface HtmlAttributes { disabled: boolean
} interface ButtonHtmlAttributes extends HtmlAttributes { type: 'Submit' | 'Button' | null
}

Но типы нельзя расширять, как интерфейсы. Вам необходимо использовать символ пересечения (&) следующим образом:

type HtmlAttributes = { disabled: boolean
} type ButtonHtmlAttributes = HtmlAttributes & { type: 'Submit' | 'Button' | null
}

Далее, объявление интерфейса всегда является объектом, а объявление типа может иметь примитивные значения, как показано ниже:

type isLoading = boolean
type Theme = "dark" | "light"
type Lang = "en" | "fr"

Ни один из приведенных выше примеров невозможен с интерфейсом, поэтому тип может быть предпочтительным для простых значений объекта. Вопрос в том, когда использовать одно вместо другого? Снова из Руководства по TypeScript: Если вам нужна эвристика, используйте interface, пока вам не понадобится использовать функции type.

Анализатор кода TypeScript сообщит вам, когда вам нужно использовать interface или type. Если вы не уверены, какой из них использовать, всегда выбирайте interface, пока не увидите причину использования type. Если вам нужна дополнительная информация, вот ответ StackOverflow, в котором сравниваются interface и type.

Надеюсь, эта шпаргалка будет полезна для вашего следующего проекта.

Автор: Nathan Sebhastian

Источник: blog.bitsrc.io

Редакция: Команда webformyself.

Читайте нас в Telegram, VK, Яндекс.Дзен