Ir al contenido principal
Traducción Beta No Oficial

Esta página fue traducida por PageTurner AI (beta). No está respaldada oficialmente por el proyecto. ¿Encontraste un error? Reportar problema →

Uso con TypeScript

Qué aprenderás
  • Patrones estándar para configurar una app Redux con TypeScript
  • Técnicas para tipar correctamente partes de la lógica Redux
Prerrequisitos

Resumen

TypeScript es un superset tipado de JavaScript que proporciona verificación de código en tiempo de compilación. Al usarse con Redux, TypeScript puede ayudar a proporcionar:

  1. Seguridad de tipos para reductores, estado, creadores de acciones y componentes UI

  2. Refactorización sencilla de código tipado

  3. Una experiencia de desarrollo superior en entornos de equipo

Recomendamos encarecidamente usar TypeScript en aplicaciones Redux. Sin embargo, como todas las herramientas, TypeScript tiene contrapartidas. Añade complejidad en términos de escribir código adicional, entender la sintaxis de TS y construir la aplicación. Al mismo tiempo, proporciona valor al detectar errores antes en el desarrollo, permitiendo refactorizaciones más seguras y eficientes, y actuando como documentación del código existente.

Creemos que el uso pragmático de TypeScript proporciona suficiente valor y beneficio para justificar la sobrecarga añadida, especialmente en bases de código grandes, pero deberías tomarte tiempo para evaluar las contrapartidas y decidir si merece la pena usar TS en tu propia aplicación.

Existen múltiples enfoques posibles para la verificación de tipos en código Redux. Esta página muestra nuestros patrones recomendados estándar para usar Redux y TypeScript juntos, y no es una guía exhaustiva. Seguir estos patrones debería resultar en una buena experiencia de uso de TS, con el mejor equilibrio entre seguridad de tipos y cantidad de declaraciones de tipos que añades a tu base de código.

Configuración estándar de proyecto Redux Toolkit con TypeScript

Asumimos que un proyecto Redux típico usa Redux Toolkit y React Redux juntos.

Redux Toolkit (RTK) es el enfoque estándar para escribir lógica Redux moderna. RTK ya está escrito en TypeScript, y su API está diseñada para proporcionar una buena experiencia de uso con TypeScript.

React Redux tiene sus definiciones de tipos en un paquete separado @types/react-redux en NPM. Además de tipar las funciones de la librería, los tipos también exportan ayudantes para facilitar la escritura de interfaces seguras entre tu store Redux y tus componentes React.

Desde React Redux v7.2.3, el paquete react-redux tiene una dependencia en @types/react-redux, por lo que las definiciones de tipos se instalarán automáticamente con la librería. En caso contrario, necesitarás instalarlas manualmente (normalmente npm install @types/react-redux).

La plantilla Redux+TS para Create-React-App incluye un ejemplo funcional de estos patrones ya configurado.

Definir Tipos de Estado Raíz y Dispatch

Usar configureStore no debería requerir tipados adicionales. Sin embargo, querrás extraer el tipo RootState y el tipo Dispatch para poder referenciarlos según sea necesario. Inferir estos tipos desde el propio store significa que se actualizan correctamente al añadir más slices de estado o modificar configuraciones de middleware.

Dado que son tipos, es seguro exportarlos directamente desde tu archivo de configuración del store (como app/store.ts) e importarlos en otros archivos.

app/store.ts
import { configureStore } from '@reduxjs/toolkit'
// ...

export const store = configureStore({
reducer: {
posts: postsReducer,
comments: commentsReducer,
users: usersReducer
}
})

// Get the type of our store variable
export type AppStore = typeof store
// Infer the `RootState` and `AppDispatch` types from the store itself
export type RootState = ReturnType<AppStore['getState']>
// Inferred type: {posts: PostsState, comments: CommentsState, users: UsersState}
export type AppDispatch = AppStore['dispatch']

Definir Hooks Tipados

Aunque es posible importar los tipos RootState y AppDispatch en cada componente, es mejor crear versiones pre-tipadas de los hooks useDispatch y useSelector para usar en tu aplicación. Esto es importante por varias razones:

  • Para useSelector, evita tener que escribir (state: RootState) cada vez

  • Para useDispatch, el tipo Dispatch predeterminado no reconoce thunks u otros middleware. Para despachar thunks correctamente, debes usar el tipo personalizado AppDispatch del store que incluye los tipos de middleware thunk, y usarlo con useDispatch. Añadir un hook useDispatch pre-tipado evita que olvides importar AppDispatch donde sea necesario.

Dado que son variables reales y no tipos, es importante definirlas en un archivo separado como app/hooks.ts, no en el archivo de configuración del store. Esto permite importarlas en cualquier componente que necesite usar los hooks y evita posibles problemas de dependencias circulares en las importaciones.

.withTypes()

Anteriormente, el enfoque para "pre-tipar" hooks con la configuración de tu aplicación era algo variado. El resultado se vería similar al siguiente fragmento:

app/hooks.ts
import type { TypedUseSelectorHook } from 'react-redux'
import { useDispatch, useSelector, useStore } from 'react-redux'
import type { AppDispatch, AppStore, RootState } from './store'

// Use throughout your app instead of plain `useDispatch` and `useSelector`
export const useAppDispatch: () => AppDispatch = useDispatch
export const useAppSelector: TypedUseSelectorHook<RootState> = useSelector
export const useAppStore: () => AppStore = useStore

React Redux v9.1.0 añade un nuevo método .withTypes a cada uno de estos hooks, análogo al método .withTypes encontrado en createAsyncThunk de Redux Toolkit.

La configuración ahora se convierte en:

app/hooks.ts
import { useDispatch, useSelector, useStore } from 'react-redux'
import type { AppDispatch, AppStore, RootState } from './store'

// Use throughout your app instead of plain `useDispatch` and `useSelector`
export const useAppDispatch = useDispatch.withTypes<AppDispatch>()
export const useAppSelector = useSelector.withTypes<RootState>()
export const useAppStore = useStore.withTypes<AppStore>()

Uso en la Aplicación

Definir Tipos de Estado y Acciones del Slice

Cada archivo de slice debe definir un tipo para su estado inicial, para que createSlice pueda inferir correctamente el tipo de state en cada reducer case.

Todas las acciones generadas deben definirse usando el tipo PayloadAction<T> de Redux Toolkit, que toma como argumento genérico el tipo del campo action.payload.

Puedes importar con seguridad el tipo RootState desde el archivo del store aquí. Es una importación circular, pero el compilador de TypeScript puede manejarlo correctamente para tipos. Esto puede ser necesario para casos de uso como escribir funciones selectoras.

features/counter/counterSlice.ts
import { createSlice, PayloadAction } from '@reduxjs/toolkit'
import type { RootState } from '../../app/store'

// Define a type for the slice state
interface CounterState {
value: number
}

// Define the initial state using that type
const initialState: CounterState = {
value: 0
}

export const counterSlice = createSlice({
name: 'counter',
// `createSlice` will infer the state type from the `initialState` argument
initialState,
reducers: {
increment: state => {
state.value += 1
},
decrement: state => {
state.value -= 1
},
// Use the PayloadAction type to declare the contents of `action.payload`
incrementByAmount: (state, action: PayloadAction<number>) => {
state.value += action.payload
}
}
})

export const { increment, decrement, incrementByAmount } = counterSlice.actions

// Other code such as selectors can use the imported `RootState` type
export const selectCount = (state: RootState) => state.counter.value

export default counterSlice.reducer

Los creadores de acciones generados estarán correctamente tipados para aceptar un argumento payload basado en el tipo PayloadAction<T> que proporcionaste para el reducer. Por ejemplo, incrementByAmount requiere un number como argumento.

En algunos casos, TypeScript puede restringir innecesariamente el tipo del estado inicial. Si esto ocurre, puedes solucionarlo haciendo una conversión de tipo del estado inicial usando as, en lugar de declarar el tipo de la variable:

// Workaround: cast state instead of declaring variable type
const initialState = {
value: 0
} as CounterState

Usar hooks tipados en componentes

En archivos de componentes, importa los hooks pre-tipados en lugar de los hooks estándar de React Redux.

features/counter/Counter.tsx
import React, { useState } from 'react'

import { useAppSelector, useAppDispatch } from 'app/hooks'

import { decrement, increment } from './counterSlice'

export function Counter() {
// The `state` arg is correctly typed as `RootState` already
const count = useAppSelector(state => state.counter.value)
const dispatch = useAppDispatch()

// omit rendering logic
}
Advertencia sobre importaciones incorrectas

ESLint puede ayudar a tu equipo a importar los hooks correctos fácilmente. La regla typescript-eslint/no-restricted-imports puede mostrar una advertencia cuando se usa accidentalmente la importación incorrecta.

Puedes añadir esto a tu configuración de ESLint como ejemplo:

"no-restricted-imports": "off",
"@typescript-eslint/no-restricted-imports": [
"warn",
{
"name": "react-redux",
"importNames": ["useSelector", "useDispatch"],
"message": "Use typed hooks `useAppDispatch` and `useAppSelector` instead."
}
],

Tipado de lógica adicional de Redux

Verificación de tipos en los reducers

Los reducers son funciones puras que reciben el state actual y la action entrante como argumentos, y devuelven un nuevo estado.

Si usas createSlice de Redux Toolkit, rara vez necesitarás tipar un reducer por separado. Si escribes un reducer independiente, normalmente es suficiente declarar el tipo del valor initialState y tipar la action como UnknownAction:

import { UnknownAction } from 'redux'

interface CounterState {
value: number
}

const initialState: CounterState = {
value: 0
}

export default function counterReducer(
state = initialState,
action: UnknownAction
) {
// logic here
}

Sin embargo, el núcleo de Redux también exporta un tipo Reducer<State, Action> que puedes usar.

Verificación de tipos en el middleware

El middleware es un mecanismo de extensión para el store de Redux. Los middleware se componen en una canalización que envuelve el método dispatch del store, y tienen acceso a los métodos dispatch y getState del store.

El núcleo de Redux exporta un tipo Middleware que se puede usar para tipar correctamente una función middleware:

export interface Middleware<
DispatchExt = {}, // optional override return behavior of `dispatch`
S = any, // type of the Redux store state
D extends Dispatch = Dispatch // type of the dispatch method
>

Un middleware personalizado debe usar el tipo Middleware y pasar los argumentos genéricos para S (state) y D (dispatch) si es necesario:

import { Middleware } from 'redux'

import { RootState } from '../store'

export const exampleMiddleware: Middleware<
{}, // Most middleware do not modify the dispatch return value
RootState
> = storeApi => next => action => {
const state = storeApi.getState() // correctly typed as RootState
}
precaución

Si usas typescript-eslint, la regla @typescript-eslint/ban-types podría reportar un error si usas {} para el valor de dispatch. Los cambios que recomienda son incorrectos y romperán los tipos de tu store de Redux. Debes desactivar la regla para esta línea y seguir usando {}.

El genérico de dispatch probablemente solo sea necesario si estás despachando thunks adicionales dentro del middleware.

En casos donde se usa type RootState = ReturnType<typeof store.getState>, se puede evitar una referencia circular de tipos entre las definiciones del middleware y el store cambiando la definición de tipo de RootState a:

const rootReducer = combineReducers({ ... });
type RootState = ReturnType<typeof rootReducer>;

Cambiando la definición de tipo de RootState con un ejemplo de Redux Toolkit:

// instead of defining the reducers in the reducer field of configureStore, combine them here:
const rootReducer = combineReducers({ counter: counterReducer })

// then set rootReducer as the reducer object of configureStore
const store = configureStore({
reducer: rootReducer,
middleware: getDefaultMiddleware =>
getDefaultMiddleware().concat(yourMiddleware)
})

type RootState = ReturnType<typeof rootReducer>

Verificación de tipos en los thunks de Redux

Redux Thunk es el middleware estándar para escribir lógica síncrona y asíncrona que interactúa con el store de Redux. Una función thunk recibe dispatch y getState como parámetros. Redux Thunk tiene un tipo ThunkAction integrado que podemos usar para definir tipos para esos argumentos:

export type ThunkAction<
R, // Return type of the thunk function
S, // state type used by getState
E, // any "extra argument" injected into the thunk
A extends Action // known types of actions that can be dispatched
> = (dispatch: ThunkDispatch<S, E, A>, getState: () => S, extraArgument: E) => R

Normalmente querrás proporcionar los argumentos genéricos R (tipo de retorno) y S (estado). Desafortunadamente, TS no permite proporcionar solo algunos argumentos genéricos, por lo que los valores habituales para los otros argumentos son unknown para E y UnknownAction para A:

import { UnknownAction } from 'redux'
import { sendMessage } from './store/chat/actions'
import { RootState } from './store'
import { ThunkAction } from 'redux-thunk'

export const thunkSendMessage =
(message: string): ThunkAction<void, RootState, unknown, UnknownAction> =>
async dispatch => {
const asyncResp = await exampleAPI()
dispatch(
sendMessage({
message,
user: asyncResp,
timestamp: new Date().getTime()
})
)
}

function exampleAPI() {
return Promise.resolve('Async Chat Bot')
}

Para reducir la repetición, puedes definir un tipo reutilizable AppThunk una vez, en tu archivo de store, y luego usar ese tipo cada vez que escribas un thunk:

export type AppThunk<ReturnType = void> = ThunkAction<
ReturnType,
RootState,
unknown,
UnknownAction
>

Ten en cuenta que esto asume que no hay un valor de retorno significativo del thunk. Si tu thunk devuelve una promesa y quieres usar la promesa devuelta después de despachar el thunk, deberías usar AppThunk<Promise<SomeReturnType>>.

precaución

No olvides que el hook useDispatch por defecto no reconoce thunks, por lo que despachar un thunk causará un error de tipo. Asegúrate de usar una forma actualizada de Dispatch en tus componentes que reconozca los thunks como un tipo aceptable para despachar.

Uso con React Redux

Aunque React Redux es una biblioteca separada de Redux en sí, se usa comúnmente con React.

Para una guía completa sobre cómo usar correctamente React Redux con TypeScript, consulta la página "Tipado Estático" en la documentación de React Redux. Esta sección destacará los patrones estándar.

Si estás usando TypeScript, los tipos de React Redux se mantienen por separado en DefinitelyTyped, pero están incluidos como dependencia del paquete react-redux, por lo que deberían instalarse automáticamente. Si aún necesitas instalarlos manualmente, ejecuta:

npm install @types/react-redux

Tipar el hook useSelector

Declara el tipo del parámetro state en la función selector, y el tipo de retorno de useSelector se inferirá para que coincida con el tipo de retorno del selector:

interface RootState {
isOn: boolean
}

// TS infers type: (state: RootState) => boolean
const selectIsOn = (state: RootState) => state.isOn

// TS infers `isOn` is boolean
const isOn = useSelector(selectIsOn)

Esto también se puede hacer en línea:

const isOn = useSelector((state: RootState) => state.isOn)

Sin embargo, se prefiere crear un hook useAppSelector previamente tipado con el tipo correcto de state incorporado.

Tipar el hook useDispatch

Por defecto, el valor de retorno de useDispatch es el tipo Dispatch estándar definido por los tipos principales de Redux, por lo que no se necesitan declaraciones:

const dispatch = useDispatch()

Sin embargo, se prefiere crear un hook useAppDispatch previamente tipado con el tipo correcto de Dispatch incorporado.

Tipar el componente de orden superior connect

Si todavía estás usando connect, deberías usar el tipo ConnectedProps<T> exportado por @types/react-redux^7.1.2 para inferir automáticamente los tipos de las props de connect. Esto requiere dividir la llamada connect(mapState, mapDispatch)(MyComponent) en dos partes:

import { connect, ConnectedProps } from 'react-redux'

interface RootState {
isOn: boolean
}

const mapState = (state: RootState) => ({
isOn: state.isOn
})

const mapDispatch = {
toggleOn: () => ({ type: 'TOGGLE_IS_ON' })
}

const connector = connect(mapState, mapDispatch)

// The inferred type will look like:
// {isOn: boolean, toggleOn: () => void}
type PropsFromRedux = ConnectedProps<typeof connector>

type Props = PropsFromRedux & {
backgroundColor: string
}

const MyComponent = (props: Props) => (
<div style={{ backgroundColor: props.backgroundColor }}>
<button onClick={props.toggleOn}>
Toggle is {props.isOn ? 'ON' : 'OFF'}
</button>
</div>
)

export default connector(MyComponent)

Uso con Redux Toolkit

La sección Configuración estándar de proyecto con Redux Toolkit y TypeScript ya cubrió los patrones de uso normal para configureStore y createSlice, y la página de Redux Toolkit "Uso con TypeScript" cubre en detalle todas las APIs de RTK.

Aquí hay algunos patrones de tipado adicionales que verás comúnmente al usar RTK.

Tipar configureStore

configureStore infiere el tipo del valor de estado a partir de la función root reducer proporcionada, por lo que no deberían ser necesarias declaraciones de tipo específicas.

Si deseas añadir middleware adicional al store, asegúrate de usar los métodos especializados .concat() y .prepend() incluidos en el array devuelto por getDefaultMiddleware(), ya que estos preservarán correctamente los tipos del middleware que añadas. (Usar el operador spread de JS suele perder esos tipos).

const store = configureStore({
reducer: rootReducer,
middleware: getDefaultMiddleware =>
getDefaultMiddleware()
.prepend(
// correctly typed middlewares can just be used
additionalMiddleware,
// you can also type middlewares manually
untypedMiddleware as Middleware<
(action: Action<'specialAction'>) => number,
RootState
>
)
// prepend and concat calls can be chained
.concat(logger)
})

Coincidencia de Acciones

Los creadores de acciones generados por RTK tienen un método match que actúa como predicado de tipo. Llamar a someActionCreator.match(action) comparará la cadena action.type y, si se usa como condición, restringirá el tipo de action al tipo TS correcto:

const increment = createAction<number>('increment')
function test(action: Action) {
if (increment.match(action)) {
// action.payload inferred correctly here
const num = 5 + action.payload
}
}

Esto es especialmente útil al verificar tipos de acciones en middleware de Redux, como middleware personalizado, redux-observable y el método filter de RxJS.

Tipado de createSlice

Definición de Reducers Case Independientes

Si tienes demasiados reducers case y definirlos inline sería confuso, o quieres reutilizarlos entre slices, puedes definirlos fuera de createSlice y tiparlos como CaseReducer:

type State = number
const increment: CaseReducer<State, PayloadAction<number>> = (state, action) =>
state + action.payload

createSlice({
name: 'test',
initialState: 0,
reducers: {
increment
}
})

Tipado de extraReducers

Si añades un campo extraReducers en createSlice, usa siempre el formato "builder callback", ya que el formato "objeto plano" no infiere correctamente los tipos de acción. Pasar un creador de acciones generado por RTK a builder.addCase() inferirá correctamente el tipo de action:

const usersSlice = createSlice({
name: 'users',
initialState,
reducers: {
// fill in primary logic here
},
extraReducers: builder => {
builder.addCase(fetchUserById.pending, (state, action) => {
// both `state` and `action` are now correctly typed
// based on the slice state and the `pending` action creator
})
}
})

Tipado de Callbacks prepare

Si necesitas añadir una propiedad meta o error a tu acción, o personalizar el payload, debes usar la notación prepare para definir el reducer case. En TypeScript se vería así:

const blogSlice = createSlice({
name: 'blogData',
initialState,
reducers: {
receivedAll: {
reducer(
state,
action: PayloadAction<Page[], string, { currentPage: number }>
) {
state.all = action.payload
state.meta = action.meta
},
prepare(payload: Page[], currentPage: number) {
return { payload, meta: { currentPage } }
}
}
}
})

Corrección de Tipos Circulares en Slices Exportados

Finalmente, en raras ocasiones podrías necesitar exportar el slice reducer con un tipo específico para resolver problemas de dependencias circulares. Ejemplo:

export default counterSlice.reducer as Reducer<Counter>

Tipado de createAsyncThunk

Para uso básico, el único tipo que necesitas proporcionar para createAsyncThunk es el tipo del argumento único para tu callback de creación de payload. También asegúrate de tipar correctamente el valor de retorno:

const fetchUserById = createAsyncThunk(
'users/fetchById',
// Declare the type your function argument here:
async (userId: number) => {
const response = await fetch(`https://reqres.in/api/users/${userId}`)
// Inferred return type: Promise<MyData>
return (await response.json()) as MyData
}
)

// the parameter of `fetchUserById` is automatically inferred to `number` here
// and dispatching the resulting thunkAction will return a Promise of a correctly
// typed "fulfilled" or "rejected" action.
const lastReturnedAction = await store.dispatch(fetchUserById(3))

Si necesitas modificar los tipos del parámetro thunkApi (como el tipo de state devuelto por getState()), debes proporcionar los primeros dos argumentos genéricos (tipo de retorno y argumento payload), más los campos relevantes de "thunkApi" en un objeto:

const fetchUserById = createAsyncThunk<
// Return type of the payload creator
MyData,
// First argument to the payload creator
number,
{
// Optional fields for defining thunkApi field types
dispatch: AppDispatch
state: State
extra: {
jwt: string
}
}
>('users/fetchById', async (userId, thunkApi) => {
const response = await fetch(`https://reqres.in/api/users/${userId}`, {
headers: {
Authorization: `Bearer ${thunkApi.extra.jwt}`
}
})
return (await response.json()) as MyData
})

Tipado de createEntityAdapter

El uso de createEntityAdapter con TypeScript varía según si tus entidades se normalizan por una propiedad id o si necesitas un selectId personalizado.

Si tus entidades se normalizan por una propiedad id, createEntityAdapter solo requiere que especifiques el tipo de entidad como argumento genérico único. Ejemplo:

interface Book {
id: number
title: string
}

// no selectId needed here, as the entity has an `id` property we can default to
const booksAdapter = createEntityAdapter<Book>({
sortComparer: (a, b) => a.title.localeCompare(b.title)
})

const booksSlice = createSlice({
name: 'books',
// The type of the state is inferred here
initialState: booksAdapter.getInitialState(),
reducers: {
bookAdded: booksAdapter.addOne,
booksReceived(state, action: PayloadAction<{ books: Book[] }>) {
booksAdapter.setAll(state, action.payload.books)
}
}
})

Si necesitas normalizar por una propiedad diferente, te recomendamos pasar una función selectId personalizada y anotar allí. Esto permite inferir correctamente el tipo del ID sin tener que proporcionarlo manualmente.

interface Book {
bookId: number
title: string
// ...
}

const booksAdapter = createEntityAdapter({
selectId: (book: Book) => book.bookId,
sortComparer: (a, b) => a.title.localeCompare(b.title)
})

const booksSlice = createSlice({
name: 'books',
// The type of the state is inferred here
initialState: booksAdapter.getInitialState(),
reducers: {
bookAdded: booksAdapter.addOne,
booksReceived(state, action: PayloadAction<{ books: Book[] }>) {
booksAdapter.setAll(state, action.payload.books)
}
}
})

Recomendaciones Adicionales

Usa la API de Hooks de React Redux

Recomendamos usar la API de hooks de React Redux como enfoque predeterminado. La API de hooks es mucho más simple con TypeScript: useSelector es un hook simple que toma una función selector, y su tipo de retorno se infiere fácilmente del tipo del argumento state.

Aunque connect sigue funcionando y puede tiparse, es mucho más complejo de tipar correctamente.

Evita Uniones de Tipos de Acción

Recomendamos específicamente en contra de intentar crear uniones de tipos de acción, ya que no proporciona ningún beneficio real y de hecho engaña al compilador en algunos aspectos. Consulta la publicación del mantenedor de RTK Lenz Weber No crear tipos unión con tipos de acción de Redux para ver una explicación de por qué esto es un problema.

Además, si estás usando createSlice, ya sabes que todas las acciones definidas por ese slice se manejan correctamente.

Recursos

Para más información, consulta estos recursos adicionales: