Перейти к содержанию

Использование TypeScript

TypeScript — это популярный способ добавления определений типов в кодовые базы JavaScript. Из коробки TypeScript поддерживает JSX, и вы можете получить полную поддержку React Web, добавив @types/react и @types/react-dom в ваш проект.

Вы узнаете

  • TypeScript с компонентами React
  • Примеры типизации с помощью хуков
  • Общие типы из @types/react
  • Дополнительные источники обучения

Установка

Все продуктовые React-фреймворки поддерживают использование TypeScript. Для установки следуйте руководству по конкретным фреймворкам:

Добавление TypeScript в существующий проект React

Чтобы установить последнюю версию определений типов React:

npm install @types/react @types/react-dom

В файле tsconfig.json необходимо установить следующие параметры компилятора:

  1. dom должен быть включен в lib (Примечание: Если опция lib не указана, dom будет включен по умолчанию).
  2. Для параметра jsx должна быть установлена одна из допустимых опций. Для большинства приложений достаточно preserve.

    Если вы публикуете библиотеку, обратитесь к документации jsx, чтобы узнать, какое значение выбрать.

TypeScript с компонентами React

Каждый файл, содержащий JSX, должен использовать расширение .tsx. Это специфическое для TypeScript расширение, которое сообщает TypeScript, что этот файл содержит JSX.

Написание TypeScript на React очень похоже на написание JavaScript на React. Ключевое отличие при работе с компонентом заключается в том, что вы можете предоставлять типы для реквизитов компонента. Эти типы можно использовать для проверки корректности и создания встроенной документации в редакторах.

Взяв компонент MyButton из руководства Быстрый старт, мы можем добавить тип, описывающий title для кнопки:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
function MyButton({ title }: { title: string }) {
    return <button>{title}</button>;
}

export default function MyApp() {
    return (
        <div>
            <h1>Welcome to my app</h1>
            <MyButton title="I'm a button" />
        </div>
    );
}

Песочницы

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

Этот встроенный синтаксис - самый простой способ предоставить типы для компонента, но когда у вас появляется несколько полей для описания, он может стать громоздким. Вместо этого вы можете использовать interface или type для описания свойств компонента:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
interface MyButtonProps {
    /** The text to display inside the button */
    title: string;
    /** Whether the button can be interacted with */
    disabled: boolean;
}

function MyButton({ title, disabled }: MyButtonProps) {
    return <button disabled={disabled}>{title}</button>;
}

export default function MyApp() {
    return (
        <div>
            <h1>Welcome to my app</h1>
            <MyButton
                title="I'm a disabled button"
                disabled={true}
            />
        </div>
    );
}

Тип, описывающий свойства вашего компонента, может быть простым или сложным, как вам нужно, но они должны быть объектным типом, описанным с помощью type или interface. Вы можете узнать о том, как TypeScript описывает объекты в Object Types, но вам также может быть интересно использовать Union Types для описания свойства, которое может быть одним из нескольких различных типов, и руководство Creating Types from Types для более сложных случаев использования.

Примеры хуков

Определения типов из @types/react включают типы для встроенных хуков, так что вы можете использовать их в своих компонентах без дополнительных настроек. Они созданы с учетом кода, который вы пишете в своем компоненте, поэтому вы будете получать выведенные типы большую часть времени, и в идеале вам не нужно будет заниматься тонкостями создания типов.

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

useState

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

1
2
// Infer the type as "boolean"
const [enabled, setEnabled] = useState(false);

присвоит enabled тип boolean, а setEnabled будет функцией, принимающей либо аргумент boolean, либо функцию, возвращающую boolean. Если вы хотите явно указать тип состояния, вы можете сделать это, указав тип аргумента в вызове useState:

1
2
// Explicitly set the type to "boolean"
const [enabled, setEnabled] = useState<boolean>(false);

В данном случае это не очень полезно, но часто тип может потребоваться, если у вас есть тип объединения. Например, status здесь может быть одной из нескольких различных строк:

1
2
3
type Status = 'idle' | 'loading' | 'success' | 'error';

const [status, setStatus] = useState<Status>('idle');

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

1
2
3
4
5
6
7
8
9
type RequestState =
    | { status: 'idle' }
    | { status: 'loading' }
    | { status: 'success'; data: any }
    | { status: 'error'; error: Error };

const [requestState, setRequestState] = useState<
    RequestState
>({ status: 'idle' });

useReducer

Хук useReducer - это более сложный хук, который принимает функцию редуктора и начальное состояние. Типы для функции редуктора выводятся из начального состояния. Вы можете указать тип в аргументе в вызове useReducer, чтобы задать тип для состояния, но часто лучше установить тип в начальном состоянии:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
import { useReducer } from 'react';

interface State {
    count: number;
}

type CounterAction =
    | { type: 'reset' }
    | { type: 'setCount'; value: State['count'] };

const initialState: State = { count: 0 };

function stateReducer(
    state: State,
    action: CounterAction
): State {
    switch (action.type) {
        case 'reset':
            return initialState;
        case 'setCount':
            return { ...state, count: action.value };
        default:
            throw new Error('Unknown action');
    }
}

export default function App() {
    const [state, dispatch] = useReducer(
        stateReducer,
        initialState
    );

    const addFive = () =>
        dispatch({
            type: 'setCount',
            value: state.count + 5,
        });
    const reset = () => dispatch({ type: 'reset' });

    return (
        <div>
            <h1>Welcome to my counter</h1>

            <p>Count: {state.count}</p>
            <button onClick={addFive}>Add 5</button>
            <button onClick={reset}>Reset</button>
        </div>
    );
}

Мы используем TypeScript в нескольких ключевых местах:

  • interface State описывает форму состояния редуктора.
  • type CounterAction описывает различные действия, которые могут быть отправлены редуктору.
  • const initialState: State предоставляет тип для начального состояния, а также тип, который используется useReducer по умолчанию.
  • stateReducer(state: State, action: CounterAction): State устанавливает типы для аргументов и возвращаемого значения функции reducer.

Более явной альтернативой установке типа в initialState является предоставление аргумента типа в useReducer:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
import {
    stateReducer,
    State,
} from './your-reducer-implementation';

const initialState = { count: 0 };

export default function App() {
    const [state, dispatch] = useReducer<State>(
        stateReducer,
        initialState
    );
}

useContext

Хук useContext - это техника передачи данных по дереву компонентов без необходимости передавать реквизиты через компоненты. Она используется при создании компонента-провайдера и часто при создании хука для потребления значения в дочернем компоненте.

Тип значения, предоставляемого контекстом, выводится из значения, переданного в вызове createContext:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
import { createContext, useContext, useState } from 'react';

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

const useGetTheme = () => useContext(ThemeContext);

export default function MyApp() {
    const [theme, setTheme] = useState<Theme>('light');

    return (
        <ThemeContext.Provider value={theme}>
            <MyComponent />
        </ThemeContext.Provider>
    );
}

function MyComponent() {
    const theme = useGetTheme();

    return (
        <div>
            <p>Current theme: {theme}</p>
        </div>
    );
}

Эта техника работает, когда у вас есть значение по умолчанию, которое имеет смысл - но иногда бывают случаи, когда это не так, и в этих случаях null может показаться разумным значением по умолчанию. Однако, чтобы система типов могла понять ваш код, вам нужно явно установить ContextShape | null в createContext.

Это приводит к тому, что вам нужно исключить | null в типе для потребителей контекста. Мы рекомендуем, чтобы хук выполнял проверку на существование этого типа во время выполнения и выбрасывал ошибку при его отсутствии:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
import {
    createContext,
    useContext,
    useState,
    useMemo,
} from 'react';

// This is a simpler example, but you can
// imagine a more complex object here
type ComplexObject = {
    kind: string,
};

// The context is created with `| null` in the type,
// to accurately reflect the default value.
const Context =
    (createContext < ComplexObject) | (null > null);

// The `| null` will be removed via the check in the Hook.
const useGetComplexObject = () => {
    const object = useContext(Context);
    if (!object) {
        throw new Error(
            'useGetComplexObject must be used within a Provider'
        );
    }
    return object;
};

export default function MyApp() {
    const object = useMemo(() => ({ kind: 'complex' }), []);

    return (
        <Context.Provider value={object}>
            <MyComponent />
        </Context.Provider>
    );
}

function MyComponent() {
    const object = useGetComplexObject();

    return (
        <div>
            <p>Current object: {object.kind}</p>
        </div>
    );
}

useMemo

Хуки useMemo создают/восстанавливают доступ к запомненному значению из вызова функции, перезапуская функцию только при изменении зависимостей, переданных в качестве второго параметра. Результат вызова хука определяется по возвращаемому значению функции в первом параметре. Вы можете быть более явными, указав аргумент типа в Hook.

1
2
3
4
5
6
// The type of visibleTodos is inferred from
// the return value of filterTodos
const visibleTodos = useMemo(
    () => filterTodos(todos, tab),
    [todos, tab]
);

useCallback

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

1
2
3
const handleClick = useCallback(() => {
    // ...
}, [todos]);

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

В зависимости от ваших предпочтений в стиле кода, вы можете использовать функции *EventHandler из типов React, чтобы задать тип обработчика события одновременно с определением обратного вызова:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
import { useState, useCallback } from 'react';

export default function Form() {
    const [value, setValue] = useState('Change me');

    const handleChange = useCallback<
        React.ChangeEventHandler<HTMLInputElement>
    >(
        (event) => {
            setValue(event.currentTarget.value);
        },
        [setValue]
    );

    return (
        <>
            <input value={value} onChange={handleChange} />
            <p>Value: {value}</p>
        </>
    );
}

Полезные типы

Существует довольно обширный набор типов из пакета @types/react, его стоит прочитать, когда вы почувствуете, как взаимодействуют React и TypeScript. Вы можете найти их в папке React в DefinitelyTyped. Здесь мы рассмотрим несколько наиболее распространенных типов.

События DOM

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

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
import { useState } from 'react';

export default function Form() {
    const [value, setValue] = useState('Change me');

    function handleChange(
        event: React.ChangeEvent<HTMLInputElement>
    ) {
        setValue(event.currentTarget.value);
    }

    return (
        <>
            <input value={value} onChange={handleChange} />
            <p>Value: {value}</p>
        </>
    );
}

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

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

Если вам нужно использовать событие, которое не включено в этот список, вы можете использовать тип React.SyntheticEvent, который является базовым типом для всех событий.

Children

Существует два общих способа описания дочерних элементов компонента. Первый - использовать тип React.ReactNode, который представляет собой объединение всех возможных типов, которые могут быть переданы в качестве дочерних в JSX:

1
2
3
4
interface ModalRendererProps {
    title: string;
    children: React.ReactNode;
}

Это очень широкое определение детей. Второе - использовать тип React.ReactElement, который относится только к элементам JSX, а не к примитивам JavaScript вроде строк или чисел:

1
2
3
4
interface ModalRendererProps {
    title: string;
    children: React.ReactElement;
}

Обратите внимание, что вы не можете использовать TypeScript для описания того, что дочерние элементы являются определенным типом JSX-элементов, поэтому вы не можете использовать систему типов для описания компонента, который принимает только <li> дочерние элементы.

Вы можете увидеть все примеры как React.ReactNode, так и React.ReactElement с проверкой типов в этой площадке TypeScript.

Свойства стиля

При использовании встроенных стилей в React вы можете использовать React.CSSProperties для описания объекта, передаваемого в реквизит style. Этот тип представляет собой объединение всех возможных CSS-свойств и является хорошим способом убедиться, что вы передаете действительные CSS-свойства в реквизит style, а также получить автозаполнение в вашем редакторе.

1
2
3
interface MyComponentProps {
    style: React.CSSProperties;
}

Дальнейшее обучение

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

Мы рекомендуем следующие ресурсы:

  • The TypeScript handbook - официальная документация по TypeScript, охватывающая большинство ключевых возможностей языка.

  • The TypeScript release notes подробно рассказывает о каждой новой функции.

  • React TypeScript Cheatsheet - это поддерживаемая сообществом шпаргалка по использованию TypeScript с React, охватывающая множество полезных крайних случаев и предоставляющая более широкую информацию, чем этот документ.

  • TypeScript Community Discord - отличное место, где можно задать вопросы и получить помощь по проблемам TypeScript и React.

Источник — https://react.dev/learn/typescript

Комментарии