Irbisa · Шпаргалка

TypeScript в React

Типизация props, children и событий, дженерик-компоненты, discriminated unions, утилитные типы, типизация хуков и контекста, strict

Middle-разработчик · 21 вопрос

  1. 01

    Как типизировать пропсы компонента и нужен ли для этого React.FC?

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

    interface ButtonProps {  label: string;  variant?: 'primary' | 'secondary';  disabled?: boolean;  onClick: () => void;}…
  2. 02

    Как типизировать children и чем отличаются ReactNode, ReactElement и JSX.Element?

    children типизируют как React.ReactNode, когда компонент принимает всё, что React умеет отрисовать: строки, числа, элементы, массивы, фрагменты, null, undefined и булевы значения.

    interface CardProps {  title: string;  children: React.ReactNode; // text, elements, arrays, null} function Card({ title, children }: CardProps) {…
  3. 03

    Как типизировать обработчики событий вроде onChange, onSubmit и onClick?

    Инлайн-обработчикам аннотация не нужна: TypeScript выводит тип события из JSX-атрибута.

    function SignupForm() {  const [email, setEmail] = useState('');   const handleChange = (e: React.ChangeEvent<HTMLInputElement>) => {    setEmail(e.target.value);  };…
  4. 04

    Как типизировать useState, если начальное значение не описывает всех возможных состояний?

    Тип передают явно через дженерик: useState<User | null>(null), useState<string[]>([]).

    interface User {  id: string;  name: string;} function Profile() {…
  5. 05

    Как типизировать ref на DOM-элемент и как компонент принимает ref в React 19?

    Ref на DOM типизируют элементом и начальным null: у useRef<HTMLInputElement>(null) поле .current имеет тип HTMLInputElement | null, и TypeScript заставляет его проверить.

    interface TextInputProps extends React.ComponentProps<'input'> {  label: string;} // React 19: ref arrives as a regular prop, no forwardReffunction TextInput({ label, ref, ...rest }: TextInputProps) {…
  6. 06

    Как типизировать компонент-обёртку, который принимает все пропсы нативного элемента, например Button поверх <button>?

    Пропсы строят из React.ComponentProps<'button'> и пересекают со своими: тогда обёртка принимает каждый атрибут, событие и ref, которые принимает <button>, а остальное раскладывается на элемент.

    type ButtonProps = React.ComponentProps<'button'> & {  variant?: 'primary' | 'ghost';}; function Button({ variant = 'primary', className, ...rest }: ButtonProps) {  return <button className={`btn btn-${variant} ${className ?? ''}`} {...rest} />;…
  7. 07

    Что такое размеченное объединение (discriminated union) и как оно помогает типизировать состояние и пропсы компонента?

    Размеченное объединение состоит из объектных типов с общим литеральным полем (дискриминантом, например status), поэтому проверка этого поля сужает значение до одного варианта.

    type FetchState<T> =  | { status: 'idle' }  | { status: 'loading' }  | { status: 'error'; error: Error }  | { status: 'success'; data: T };…
  8. 08

    Скомпилируется ли этот компонент? Если нет, что сообщит TypeScript и как это исправить?

    type ActionProps =  | { kind: 'link'; href: string }  | { kind: 'button'; onClick: () => void }; function Action(props: ActionProps) {  const { kind, href } = props;  if (kind === 'link') {    return <a href={href}>Open</a>;  }  return <button onClick={props.onClick}>Run</button>;}

    Не скомпилируется: const { kind, href } = props выдаст "Property 'href' does not exist on type 'ActionProps'".

  9. 09

    Как написать дженерик-компонент, например список или select, который работает с любым типом элементов?

    Компонент объявляют как дженерик-функцию, function List<T>(props: ListProps<T>), и используют T в пропсах.

    interface ListProps<T> {  items: T[];  getKey: (item: T) => string;  renderItem: (item: T) => React.ReactNode;}…
  10. 10

    Как типизировать компонент высшего порядка (HOC), чтобы вызывающему не нужно было передавать пропсы, которые он внедряет?

    Сделайте HOC дженериком по пропсам оборачиваемого компонента, P extends InjectedProps, а возвращаемому компоненту дайте тип Omit<P, keyof InjectedProps>.

    interface WithUserProps {  user: User;} // P is the wrapped component's full props; the HOC supplies `user` itselffunction withUser<P extends WithUserProps>(Component: React.ComponentType<P>) {…
  11. 11

    Как типизировать контекст React, чтобы потребителям не приходилось каждый раз проверять его на null?

    Контекст создают как createContext<Value | null>(null) и отдают только через кастомный хук, который бросает ошибку, если провайдера нет.

    interface AuthContextValue {  user: User | null;  signOut: () => void;} const AuthContext = createContext<AuthContextValue | null>(null);…
  12. 12

    Как типизировать экшены useReducer и как заставить компилятор поймать необработанный экшен?

    Экшены типизируют размеченным объединением по полю type, у редьюсера аннотируют state, action и тип результата, остальное useReducer выводит сам.

    type State = { count: number; step: number };type Action =  | { type: 'increment' }  | { type: 'setStep'; step: number }  | { type: 'reset' };…
  13. 13

    Как один раз типизировать useSelector и useDispatch из Redux Toolkit для всего приложения и откуда берутся RootState и AppDispatch?

    Выводите оба типа из стора, RootState = ReturnType<typeof store.getState> и AppDispatch = typeof store.dispatch, затем один раз создайте типизированные хуки через useSelector.withTypes<RootState>() и useDispatch.withTypes<AppDispatch>() и используйте в компонентах только их.

    // app/store.tsexport const store = configureStore({  reducer: { cart: cartSlice.reducer, user: userSlice.reducer },});export type RootState = ReturnType<typeof store.getState>; // grows with every sliceexport type AppDispatch = typeof store.dispatch;            // includes the thunk overloads…
  14. 14

    Какие утилитарные типы TypeScript вы чаще всего используете в React-коде и для чего?

    Повседневный набор: Partial, Pick, Omit, Record, ReturnType, Parameters, Awaited и NonNullable, плюс собственный ComponentProps из React.

    interface User {  id: string;  name: string;  email: string;  role: 'admin' | 'member';}…
  15. 15

    Что делает оператор satisfies и чем он отличается от аннотации типа и от as?

    value satisfies T проверяет, что значение соответствует T, но сохраняет собственный, более узкий выведенный тип значения.

    type Route = { path: string; title: string }; // annotation: checked, but keys widen to stringconst annotated: Record<string, Route> = {  home: { path: '/', title: 'Home' },};…
  16. 16

    Как типизировать результат кастомного хука, который, как useState, возвращает кортеж?

    Массив возвращают с as const или явно объявляют тип кортежа.

    // wrong: inferred as (boolean | (() => void))[]function useToggleLoose(initial = false) {  const [on, setOn] = useState(initial);  return [on, () => setOn((v) => !v)];}…
  17. 17

    Найдите баг: страница профиля падает с Cannot read properties of undefined (reading 'city'), хотя TypeScript не сообщает ни об одной ошибке.

    interface User {  id: string;  name: string;  address: { city: string };} async function loadUser(id: string): Promise<User> {  const res = await fetch(`/api/users/${id}`);  return (await res.json()) as User;} function City({ user }: { user: User }) {  return <p>{user.address.city}</p>;}

    as User лишь сообщает компилятору, как выглядит JSON; в рантайме это никто не проверяет, а API возвращает пользователей без address.

  18. 18

    Что включает strict в tsconfig.json и какие дополнительные флаги стоит включить в React-проекте?

    "strict": true включает семейство проверок, главные из них strictNullChecks (null и undefined становятся отдельными типами, которые нужно обработать) и noImplicitAny (параметр без типа даёт ошибку, а не молчаливый any).

    {  "compilerOptions": {    "target": "ES2022",    "lib": ["ES2022", "DOM", "DOM.Iterable"],    "module": "ESNext",    "moduleResolution": "bundler",…
  19. 19

    Как типизировать полиморфный компонент с пропсом as, чтобы <Text as="a"> принимал href, а <Text as="span"> нет?

    Компонент делают дженериком по типу элемента, E extends React.ElementType, а его пропсы собирают из своих пропсов плюс React.ComponentProps<E> без ключей, которые вы определяете сами.

    type PolymorphicProps<E extends React.ElementType, P> = P & { as?: E } &  Omit<React.ComponentProps<E>, keyof P | 'as'>; type TextOwnProps = { size?: 'sm' | 'md' | 'lg' }; function Text<E extends React.ElementType = 'span'>({…
  20. 20

    Как типизировать пропсы, которые либо контролируемые (value с onChange), либо неконтролируемые (defaultValue), чтобы смешивание не компилировалось?

    Режимы описывают объединением, а пропсы чужого режима запрещают необязательными полями never: контролируемый вариант требует value и onChange и содержит defaultValue?: never, неконтролируемый содержит value?: never.

    type Controlled = {  value: string;  onChange: (value: string) => void;  defaultValue?: never;};…
  21. 21

    Найдите баг: после того как пропсы обернули в Omit<ButtonProps, 'className'>, props.href перестал существовать даже внутри ветки variant === 'link'.

    type ButtonProps =  | { variant: 'link'; href: string; className?: string }  | { variant: 'action'; onClick: () => void; className?: string }; // the primary button sets its own classNametype PrimaryButtonProps = Omit<ButtonProps, 'className'>; function PrimaryButton(props: PrimaryButtonProps) {  if (props.variant === 'link') {    return <a className="btn-primary" href={props.href}>Open</a>;  }  return <button className="btn-primary" onClick={props.onClick}>Run</button>;}

    Omit не дистрибутивен: он оставляет только ключи, общие для всех вариантов объединения, поэтому Omit<ButtonProps, 'className'> схлопывает объединение в { variant: 'link' | 'action' }, и href с onClick исчезают.