Configuración de TypeScript para proyectos modernos de React usando Webpack

Publicado: 2022-03-10
Resumen rápido ↬ Este artículo presenta Typescript, un superíndice de JavaScript que presenta la función de tipo estático para detectar errores comunes en los códigos de los desarrolladores, lo que mejora el rendimiento y, por lo tanto, da como resultado aplicaciones empresariales sólidas. También aprenderá cómo configurar TypeScript de manera eficiente en un proyecto de React mientras construimos una aplicación Money Heist Episode Picker, explorando TypeScript, ganchos de React como useReducer, useContext y Reach Router.

En esta era de desarrollo de software, JavaScript se puede usar para desarrollar casi cualquier tipo de aplicación. Sin embargo, el hecho de que JavaScript se escriba de forma dinámica podría ser una preocupación para la mayoría de las grandes empresas, debido a su característica de verificación de tipo flexible.

Afortunadamente, no tenemos que esperar hasta que el Comité Técnico 39 de Ecma introduzca un sistema de tipos estáticos en JavaScript. Podemos usar TypeScript en su lugar.

JavaScript, al estar tipado dinámicamente, no es consciente del tipo de datos de una variable hasta que se crea una instancia de esa variable en tiempo de ejecución. Los desarrolladores que escriben grandes programas de software pueden tener una tendencia a reasignar una variable, declarada anteriormente, a un valor de un tipo diferente, sin advertencia ni problema alguno, lo que genera errores que a menudo se pasan por alto.

En este tutorial, aprenderemos qué es TypeScript y cómo trabajar con él en un proyecto de React. Al final, habremos creado un proyecto que consiste en una aplicación de selección de episodios para el programa de televisión Money Heist , utilizando TypeScript y ganchos similares a React actuales ( useState , useEffect , useReducer , useContext ). Con este conocimiento, puede continuar experimentando con TypeScript en sus propios proyectos.

Este artículo no es una introducción a TypeScript. Por lo tanto, no revisaremos la sintaxis básica de TypeScript y JavaScript. Sin embargo, no tienes que ser un experto en ninguno de estos idiomas para seguir, porque intentaremos seguir el principio KISS (mantenlo simple, estúpido).

¡Más después del salto! Continúe leyendo a continuación ↓

¿Qué es TypeScript?

En 2019, TypeScript se clasificó como el séptimo lenguaje más utilizado y el quinto lenguaje de más rápido crecimiento en GitHub. Pero, ¿qué es exactamente TypeScript?

De acuerdo con la documentación oficial, TypeScript es un superconjunto escrito de JavaScript que se compila en JavaScript simple. Es desarrollado y mantenido por Microsoft y la comunidad de código abierto.

"Superconjunto" en este contexto significa que el lenguaje contiene todas las características y funciones de JavaScript y algo más. TypeScript es un lenguaje de secuencias de comandos escrito.

Ofrece a los desarrolladores más control sobre su base de código a través de su anotación de tipo, clases e interfaz, lo que evita que los desarrolladores tengan que corregir manualmente los molestos errores en la consola.

TypeScript no se creó para alterar JavaScript. En su lugar, se expande en JavaScript con valiosas funciones nuevas. Cualquier programa escrito en JavaScript simple también se ejecutará como se esperaba en TypeScript, incluidas las aplicaciones móviles multiplataforma y los back-ends en Node.js.

Esto significa que también puede escribir aplicaciones React en TypeScript, como haremos en este tutorial.

¿Por qué mecanografiar?

Quizás no esté convencido de aceptar las bondades de TypeScript. Consideremos algunas de sus ventajas.

Menos errores

No podemos eliminar todos los errores en nuestro código, pero podemos reducirlos. TypeScript comprueba los tipos en tiempo de compilación y arroja errores si cambia el tipo de variable.

Ser capaz de encontrar estos errores obvios pero frecuentes tan pronto hace que sea mucho más fácil administrar su código con tipos.

La refactorización es más fácil

Probablemente a menudo desee refactorizar muchas cosas, pero debido a que tocan muchos otros códigos y muchos otros archivos, desconfía de modificarlos.

En TypeScript, estas cosas a menudo se pueden refactorizar con solo un clic en el comando "Renombrar símbolo" en su entorno de desarrollo integrado (IDE).

Cambio de nombre de la aplicación a expApp (vista previa grande)

En un lenguaje de escritura dinámica como JavaScript, la única forma de refactorizar varios archivos al mismo tiempo es con la función tradicional de "buscar y reemplazar" usando expresiones regulares (RegExp).

En un lenguaje de escritura estática como TypeScript, ya no se necesita "buscar y reemplazar". Con comandos IDE como "Buscar todas las apariciones" y "Renombrar símbolo", puede ver todas las apariciones en la aplicación de la función, clase o propiedad dada de una interfaz de objeto.

TypeScript lo ayudará a encontrar todas las instancias del bit refactorizado, cambiarle el nombre y alertarlo con un error de compilación en caso de que su código tenga algún tipo de discrepancia después de la refactorización.

TypeScript tiene incluso más ventajas que las que hemos cubierto aquí.

Desventajas de TypeScript

Seguramente TypeScript no está exento de desventajas, incluso dadas las características prometedoras destacadas anteriormente.

Una falsa sensación de seguridad

La función de verificación de tipos de TypeScript a menudo crea una falsa sensación de seguridad entre los desarrolladores. La verificación de tipos nos advierte cuando algo anda mal con nuestro código. Sin embargo, los tipos estáticos no reducen la densidad general de errores.

Por lo tanto, la fuerza de su programa dependerá de su uso de TypeScript, porque los tipos son escritos por el desarrollador y no se verifican en tiempo de ejecución.

Si está buscando TypeScript para reducir sus errores, considere el desarrollo basado en pruebas en su lugar.

Sistema de escritura complicado

El sistema de escritura, si bien es una gran herramienta en muchos aspectos, a veces puede ser un poco complicado. Esta desventaja se debe a que es totalmente interoperable con JavaScript, lo que deja aún más espacio para las complicaciones.

Sin embargo, TypeScript sigue siendo JavaScript, por lo que es importante comprender JavaScript.

¿Cuándo usar TypeScript?

Le aconsejaría que use TypeScript en los siguientes casos:

  • Si está buscando crear una aplicación que se mantendrá durante un período prolongado , le recomiendo encarecidamente que comience con TypeScript, ya que fomenta el código de autodocumentación, lo que ayuda a otros desarrolladores a comprender su código fácilmente cuando se unen a su base de código. .
  • Si necesita crear una biblioteca , considere escribirla en TypeScript. Ayudará a los editores de código a sugerir los tipos apropiados a los desarrolladores que usan su biblioteca.

En las últimas secciones, hemos equilibrado los pros y los contras de TypeScript. Pasemos al asunto del día: configurar TypeScript en un proyecto React moderno .

Empezando

Hay varias formas de configurar TypeScript en un proyecto de React. En este tutorial, cubriremos solo dos.

Método 1: crear la aplicación React + TypeScript

Hace aproximadamente dos años, el equipo de React lanzó Create React App 2.1, con soporte para TypeScript. Por lo tanto, es posible que nunca tenga que hacer ningún trabajo pesado para incluir TypeScript en su proyecto.

Anuncio de TypeScript en la aplicación Create React (vista previa grande)

Para iniciar un nuevo proyecto Create React App, puede ejecutar esto...

 npx create-react-app my-app --folder-name

… o esto:

 yarn create react-app my-app --folder-name

Para agregar TypeScript a un proyecto Create React App, primero instálelo y sus respectivos @types :

 npm install --save typescript @types/node @types/react @types/react-dom @types/jest

… o:

 yarn add typescript @types/node @types/react @types/react-dom @types/jest

A continuación, cambie el nombre de los archivos (por ejemplo, index.js a index.tsx ) y reinicie su servidor de desarrollo .

Eso fue rápido, ¿no?

Método 2: configurar TypeScript con Webpack

Webpack es un paquete de módulos estáticos para aplicaciones JavaScript. Toma todo el código de su aplicación y lo hace utilizable en un navegador web. Los módulos son fragmentos de código reutilizables creados a partir de los estilos JavaScript, node_modules , imágenes y CSS de su aplicación, que se empaquetan para usarlos fácilmente en su sitio web.

Crear un nuevo proyecto

Comencemos creando un nuevo directorio para nuestro proyecto:

 mkdir react-webpack cd react-webpack

Usaremos npm para inicializar nuestro proyecto:

 npm init -y

El comando anterior generará un archivo package.json con algunos valores predeterminados. Agreguemos también algunas dependencias para webpack, TypeScript y algunos módulos específicos de React.

Instalación de paquetes

Por último, necesitaríamos instalar los paquetes necesarios. Abra su interfaz de línea de comandos (CLI) y ejecute esto:

 #Installing devDependencies npm install --save-dev @types/react @types/react-dom awesome-typescript-loader css-loader html-webpack-plugin mini-css-extract-plugin source-map-loader typescript webpack webpack-cli webpack-dev-server #installing Dependencies npm install react react-dom

También agreguemos manualmente algunos archivos y carpetas diferentes en nuestra carpeta react-webpack :

  1. Agregue webpack.config.js para agregar configuraciones relacionadas con webpack.
  2. Agregue tsconfig.json para todas nuestras configuraciones de TypeScript.
  3. Agregue un nuevo directorio, src .
  4. Cree un nuevo directorio, components , en la carpeta src .
  5. Finalmente, agregue index.html , App.tsx e index.tsx en la carpeta de components .

Estructura del proyecto

Por lo tanto, nuestra estructura de carpetas se verá así:

 ├── package.json ├── package-lock.json ├── tsconfig.json ├── webpack.config.js ├── .gitignore └── src └──components ├── App.tsx ├── index.tsx ├── index.html

Comience a agregar algo de código

Empezaremos con index.html :

 <!DOCTYPE html> <html lang="en"> <head> <meta charset="UTF-8"> <meta name="viewport" content="width=device-width, initial-scale=1.0"> <title>React-Webpack Setup</title> </head> <body> <div></div> </body> </html>

Esto creará el HTML, con un div vacío con una ID de output .

Agreguemos el código a nuestro componente React App.tsx :

 import * as React from "react"; export interface HelloWorldProps { userName: string; lang: string; } export const App = (props: HelloWorldProps) => ( <h1> Hi {props.userName} from React! Welcome to {props.lang}! </h1> );

Creamos un objeto de interfaz y lo HelloWorldProps , con userName y lang con un tipo de string .

Pasamos props a nuestro componente de App y lo exportamos.

Ahora, actualicemos el código en index.tsx :

 import * as React from "react"; import * as ReactDOM from "react-dom"; import { App } from "./App"; ReactDOM.render( <App userName="Beveloper" lang="TypeScript" />, document.getElementById("output") );

Acabamos de importar el componente de la App en index.tsx . Cuando webpack ve cualquier archivo con la extensión .ts o .tsx , transpilará ese archivo usando la biblioteca Awesome-TypeScript-Loader.

Configuración de mecanografiado

Luego agregaremos alguna configuración a tsconfig.json :

 { "compilerOptions": { "jsx": "react", "module": "commonjs", "noImplicitAny": true, "outDir": "./build/", "preserveConstEnums": true, "removeComments": true, "sourceMap": true, "target": "es5" }, "include": [ "src/components/index.tsx" ] }

También veamos las diferentes opciones que agregamos a tsconfig.json :

  • compilerOptions Representa las diferentes opciones del compilador.
  • jsx:react Agrega soporte para JSX en archivos .tsx .
  • lib Agrega una lista de archivos de biblioteca a la compilación (por ejemplo, usar es2015 nos permite usar la sintaxis ECMAScript 6).
  • module Genera código de módulo.
  • noImplicitAny errores para declaraciones con any tipo implícito.
  • outDir Representa el directorio de salida.
  • sourceMap Genera un archivo .map , que puede ser muy útil para depurar la aplicación.
  • target Representa la versión de destino de ECMAScript para transpilar nuestro código (podemos agregar una versión según los requisitos específicos de nuestro navegador).
  • include Se utiliza para especificar la lista de archivos que se incluirán.

Configuración del paquete web

Agreguemos alguna configuración de paquete web a webpack.config.js .

 const path = require("path"); const HtmlWebpackPlugin = require("html-webpack-plugin"); const MiniCssExtractPlugin = require("mini-css-extract-plugin"); module.exports = { entry: "./src/components/index.tsx", target: "web", mode: "development", output: { path: path.resolve(\__dirname, "build"), filename: "bundle.js", }, resolve: { extensions: [".js", ".jsx", ".json", ".ts", ".tsx"], }, module: { rules: [ { test: /\.(ts|tsx)$/, loader: "awesome-typescript-loader", }, { enforce: "pre", test: /\.js$/, loader: "source-map-loader", }, { test: /\.css$/, loader: "css-loader", }, ], }, plugins: [ new HtmlWebpackPlugin({ template: path.resolve(\__dirname, "src", "components", "index.html"), }), new MiniCssExtractPlugin({ filename: "./src/yourfile.css", }), ], };

Veamos las diferentes opciones que hemos agregado a webpack.config.js :

  • entry Esto especifica el punto de entrada para nuestra aplicación. Puede ser un solo archivo o una matriz de archivos que queremos incluir en nuestra compilación.
  • output Contiene la configuración de salida. La aplicación analiza esto cuando intenta enviar el código incluido de nuestro proyecto al disco. La ruta representa el directorio de salida al que se enviará el código y el nombre de archivo representa el nombre de archivo del mismo. Generalmente se llama bundle.js .
  • resolve Webpack analiza este atributo para decidir si agrupar u omitir el archivo. Por lo tanto, en nuestro proyecto, webpack considerará los archivos con las extensiones .js , .jsx , .json , .ts y .tsx para agrupar.
  • module Podemos habilitar webpack para cargar un archivo en particular cuando lo solicite la aplicación, usando cargadores. Toma un objeto de reglas que especifica que:
    • cualquier archivo que termine con la extensión .tsx o .ts debe usar awesome-typescript-loader para ser cargado;
    • los archivos que terminan con la extensión .js deben cargarse con source-map-loader ;
    • los archivos que terminan con la extensión .css deben cargarse con css-loader .
  • plugins Webpack tiene sus propias limitaciones y proporciona complementos para superarlas y ampliar sus capacidades. Por ejemplo, html-webpack-plugin crea un archivo de plantilla que se representa en el navegador desde el archivo index.html en el directorio ./src/component/index.html .

MiniCssExtractPlugin representa el archivo CSS principal de la aplicación.

Agregar secuencias de comandos a package.json

Podemos agregar diferentes scripts para crear aplicaciones React en nuestro archivo package.json :

 "scripts": { "start": "webpack-dev-server --open", "build": "webpack" },

Ahora, ejecute npm start en su CLI. Si todo salió bien, deberías ver esto:

Salida de configuración de React-Webpack (vista previa grande)

Si tiene una habilidad especial para el paquete web, clone el repositorio para esta configuración y utilícelo en sus proyectos.

Creación de archivos

Cree una carpeta src y un archivo index.tsx . Este será el archivo base que renderiza React.

Ahora, si ejecutamos npm start , ejecutará nuestro servidor y abrirá una nueva pestaña. Ejecutar npm run build creará un paquete web para producción y creará una carpeta de compilación para nosotros.

Hemos visto cómo configurar TypeScript desde cero utilizando la aplicación Create React y el método de configuración del paquete web.

Una de las formas más rápidas de obtener una comprensión completa de TypeScript es convertir uno de sus proyectos React de vainilla existentes a TypeScript. Desafortunadamente, la adopción gradual de TypeScript en un proyecto Vanilla React existente es estresante porque implica tener que expulsar o cambiar el nombre de todos los archivos, lo que generaría conflictos y una solicitud de extracción gigante si el proyecto perteneciera a un equipo grande.

A continuación, veremos cómo migrar fácilmente un proyecto de React a TypeScript.

Migrar una aplicación Create React existente a TypeScript

Para que este proceso sea más manejable, lo dividiremos en pasos, lo que nos permitirá migrar en partes individuales. Estos son los pasos que seguiremos para migrar nuestro proyecto:

  1. Agregue TypeScript y tipos.
  2. Agregue tsconfig.json .
  3. Empieza pequeño.
  4. Cambie el nombre de la extensión de los archivos a .tsx .

1. Agregue TypeScript al proyecto

Primero, necesitaremos agregar TypeScript a nuestro proyecto. Suponiendo que su proyecto React se haya iniciado con Create React App, podemos ejecutar lo siguiente:

 # Using npm npm install --save typescript @types/node @types/react @types/react-dom @types/jest # Using Yarn yarn add typescript @types/node @types/react @types/react-dom @types/jest

Tenga en cuenta que todavía no hemos cambiado nada a TypeScript. Si ejecutamos el comando para iniciar el proyecto localmente ( npm start o yarn start ), nada cambia. Si ese es el caso, ¡entonces genial! Estamos listos para el siguiente paso.

2. Agregue el archivo tsconfig.json

Antes de aprovechar TypeScript, debemos configurarlo a través del archivo tsconfig.json . La forma más sencilla de comenzar es crear un andamio con este comando:

 npx tsc --init

Esto nos da algunos conceptos básicos, con mucho código comentado. Ahora, reemplace todo el código en tsconfig.json con esto:

 { "compilerOptions": { "jsx": "react", "module": "commonjs", "noImplicitAny": true, "outDir": "./build/", "preserveConstEnums": true, "removeComments": true, "sourceMap": true, "target": "es5" }, "include": [ "./src/**/**/\*" ] }

Configuración de mecanografiado

También veamos las diferentes opciones que agregamos a tsconfig.json :

  • compilerOptions Representa las diferentes opciones del compilador.
    • target Traduce construcciones de JavaScript más nuevas a una versión anterior, como ECMAScript 5.
    • lib Agrega una lista de archivos de biblioteca a la compilación (por ejemplo, usar es2015 nos permite usar la sintaxis ECMAScript 6).
    • jsx:react Agrega soporte para JSX en archivos .tsx .
    • lib Agrega una lista de archivos de biblioteca a la compilación (por ejemplo, usar es2015 nos permite usar la sintaxis ECMAScript 6).
    • module Genera código de módulo.
    • noImplicitAny Se utiliza para generar errores para declaraciones con any tipo implícito.
    • outDir Representa el directorio de salida.
    • sourceMap Genera un archivo .map , que puede ser muy útil para depurar nuestra aplicación.
    • include Se utiliza para especificar la lista de archivos que se incluirán.

Las opciones de configuración variarán, según la demanda de un proyecto. Es posible que deba consultar la hoja de cálculo de opciones de TypeScript para averiguar qué encajaría con su proyecto.

Solo hemos tomado las medidas necesarias para preparar las cosas. Nuestro siguiente paso es migrar un archivo a TypeScript.

3. Comience con un componente simple

Aproveche la capacidad de TypeScript para adoptarse gradualmente. Vaya un archivo a la vez a su propio ritmo. Haz lo que tenga sentido para ti y tu equipo. No intentes abordarlo todo de una vez.

Para convertir correctamente esto, tenemos que hacer dos cosas:

  1. Cambie la extensión del archivo a .tsx .
  2. Agregue la anotación de tipo (que requeriría algunos conocimientos de TypeScript).

4. Cambiar el nombre de las extensiones de archivo a .tsx

En una base de código grande, puede parecer agotador cambiar el nombre de los archivos individualmente.

Cambiar el nombre de múltiples archivos en macOS

Cambiar el nombre de varios archivos puede ser una pérdida de tiempo. Así es como puedes hacerlo en una Mac. Haga clic derecho (o Ctrl + clic, o haga clic con dos dedos simultáneamente en el panel táctil si está usando una MacBook) en la carpeta que contiene los archivos que desea cambiar de nombre. Luego, haga clic en "Mostrar en Finder". En el Finder, seleccione todos los archivos que desea cambiar de nombre. Haga clic con el botón derecho en los archivos seleccionados y elija "Renombrar elementos X..." Luego, verá algo como esto:

Cambiar el nombre de los archivos en una Mac (vista previa grande)

Inserte la cadena que desea encontrar y la cadena con la que desea reemplazar la cadena encontrada, y presione "Cambiar nombre". Hecho.

Cambiar el nombre de varios archivos en Windows

Cambiar el nombre de varios archivos en Windows está más allá del alcance de este tutorial, pero hay una guía completa disponible. Por lo general, obtendría errores después de cambiar el nombre de los archivos; solo necesita agregar las anotaciones de tipo. Puede repasar esto en la documentación.

Hemos cubierto cómo configurar TypeScript en una aplicación React. Ahora, construyamos una aplicación de selección de episodios para Money Heist usando TypeScript.

No cubriremos los tipos básicos de TypeScript. Es necesario revisar la documentación antes de continuar con este tutorial.

Hora de construir

Para que este proceso sea menos abrumador, lo dividiremos en pasos, lo que nos permitirá crear la aplicación en partes individuales. Estos son todos los pasos que seguiremos para crear el selector de episodios de La Casa de Papel:

  • Scaffold una aplicación Create React.
  • Obtener episodios.
    • Cree los tipos e interfaces apropiados para nuestros episodios en interface.ts .
    • Configure la tienda para obtener episodios en store.tsx .
    • Cree la acción para obtener episodios en action.ts .
    • Cree un componente EpisodeList.tsx que contenga los episodios obtenidos.
    • Importe el componente EpisodesList a nuestra página de inicio usando React Lazy and Suspense .
  • Añadir episodios.
    • Configure la tienda para agregar episodios en store.tsx .
    • Cree la acción para agregar episodios en action.ts .
  • Eliminar episodios.
    • Configure la tienda para eliminar episodios en store.tsx .
    • Cree la acción para eliminar episodios en action.ts .
  • Episodio favorito.
    • Importe el componente EpisodesList en el episodio favorito.
    • Render EpisodesList dentro del episodio favorito.
  • Uso de Reach Router para la navegación.

configurar reaccionar

La forma más fácil de configurar React es usar Create React App. Create React App es una forma oficialmente admitida de crear aplicaciones React de una sola página. Ofrece una configuración de construcción moderna sin configuración.

Lo usaremos para iniciar la aplicación que construiremos. Desde su CLI, ejecute el siguiente comando:

 npx create-react-app react-ts-app && cd react-ts-app

Una vez que la instalación sea exitosa, inicie el servidor React ejecutando npm start .

Página de inicio de React (vista previa grande)

Comprender las interfaces y los tipos en TypeScript

Las interfaces en TypeScript se usan cuando necesitamos dar tipos a las propiedades de los objetos. Por lo tanto, estaríamos usando interfaces para definir nuestros tipos.

 interface Employee { name: string, role: string salary: number } const bestEmployee: Employee= { name: 'John Doe', role: 'IOS Developer', salary: '$8500' //notice we are using a string }

Al compilar el código anterior, veríamos este error: “Los tipos de salary de propiedad son incompatibles. La string de tipo no se puede asignar al number de tipo ”.

Dichos errores ocurren en TypeScript cuando a una propiedad o variable se le asigna un tipo diferente al tipo definido. Específicamente, el fragmento anterior significa que a la propiedad de salary se le asignó un tipo de string en lugar de un tipo de number .

Vamos a crear un archivo interface.ts en nuestra carpeta src . Copia y pega este código en él:

 /** |-------------------------------------------------- | All the interfaces! |-------------------------------------------------- */ export interface IEpisode { airdate: string airstamp: string airtime: string id: number image: { medium: string; original: string } name: string number: number runtime: number season: number summary: string url: string } export interface IState { episodes: Array<IEpisode> favourites: Array<IEpisode> } export interface IAction { type: string payload: Array<IEpisode> | any } export type Dispatch = React.Dispatch<IAction> export type FavAction = ( state: IState, dispatch: Dispatch, episode: IEpisode ) => IAction export interface IEpisodeProps { episodes: Array<IEpisode> store: { state: IState; dispatch: Dispatch } toggleFavAction: FavAction favourites: Array<IEpisode> } export interface IProps { episodes: Array<IEpisode> store: { state: IState; dispatch: Dispatch } toggleFavAction: FavAction favourites: Array<IEpisode> }

Es una buena práctica agregar una "I" al nombre de la interfaz. Hace que el código sea legible. Sin embargo, puede decidir excluirlo.

Interfaz IEpisode

Nuestra API devuelve un conjunto de propiedades como fecha de airdate , marca de airstamp , airtime de emisión, id , image , name , number , tiempo de runtime , season , summary y url . Por lo tanto, definimos una interfaz IEpisode y establecimos los tipos de datos apropiados para las propiedades del objeto.

Interfaz de Estado

Nuestra interfaz IState tiene propiedades de episodes y favorites , respectivamente, y una interfaz Array<IEpisode> .

Acción

Las propiedades de la interfaz IAction son payload y type . La propiedad de type tiene un tipo de cadena, mientras que la carga útil tiene un tipo de Array | any Array | any

Tenga en cuenta que Array | any Array | any significa una matriz de la interfaz del episodio o cualquier tipo.

El tipo de Dispatch se establece en React.Dispatch y una interfaz <IAction> . Tenga en cuenta que React.Dispatch es el tipo estándar para la función de dispatch , de acuerdo con la base de código @types/react , mientras que <IAction> es una matriz de la acción de interfaz.

Además, Visual Studio Code tiene un comprobador de TypeScript. Entonces, simplemente resaltando o pasando el mouse sobre el código, es lo suficientemente inteligente como para sugerir el tipo apropiado.

En otras palabras, para que podamos hacer uso de nuestra interfaz en todas nuestras aplicaciones, debemos exportarla. Hasta ahora, tenemos nuestra tienda y nuestras interfaces que contienen el tipo de nuestro objeto. Ahora vamos a crear nuestra tienda. Tenga en cuenta que las otras interfaces siguen las mismas convenciones que las explicadas.

Obtener episodios

Crear una tienda

Para recuperar nuestros episodios, necesitamos un almacén que contenga el estado inicial de los datos y que defina nuestra función reductora.

Haremos uso del gancho useReducer para configurar eso. Cree un archivo store.tsx en su carpeta src . Copie y pegue el siguiente código en él.

 import React, { useReducer, createContext } from 'react' import { IState, IAction } from './types/interfaces' const initialState: IState = { episodes: [], favourites: [] } export const Store = createContext (initialState) const reducer = (state: IState, action: IAction): IState => { switch (action.type) { case 'FETCH_DATA': return { ...state, episodes: action.payload } default: return state } } export const StoreProvider = ({ children }: JSX.ElementChildrenAttribute): JSX.Element => { const [state, dispatch] = useReducer(reducer, initialState) return {children} } import React, { useReducer, createContext } from 'react' import { IState, IAction } from './types/interfaces' const initialState: IState = { episodes: [], favourites: [] } export const Store = createContext (initialState) const reducer = (state: IState, action: IAction): IState => { switch (action.type) { case 'FETCH_DATA': return { ...state, episodes: action.payload } default: return state } } export const StoreProvider = ({ children }: JSX.ElementChildrenAttribute): JSX.Element => { const [state, dispatch] = useReducer(reducer, initialState) return {children} } import React, { useReducer, createContext } from 'react' import { IState, IAction } from './types/interfaces' const initialState: IState = { episodes: [], favourites: [] } export const Store = createContext (initialState) const reducer = (state: IState, action: IAction): IState => { switch (action.type) { case 'FETCH_DATA': return { ...state, episodes: action.payload } default: return state } } export const StoreProvider = ({ children }: JSX.ElementChildrenAttribute): JSX.Element => { const [state, dispatch] = useReducer(reducer, initialState) return {children} } import React, { useReducer, createContext } from 'react' import { IState, IAction } from './types/interfaces' const initialState: IState = { episodes: [], favourites: [] } export const Store = createContext (initialState) const reducer = (state: IState, action: IAction): IState => { switch (action.type) { case 'FETCH_DATA': return { ...state, episodes: action.payload } default: return state } } export const StoreProvider = ({ children }: JSX.ElementChildrenAttribute): JSX.Element => { const [state, dispatch] = useReducer(reducer, initialState) return {children} }

Los siguientes son los pasos que hemos seguido para crear la tienda:

  • Al definir nuestra tienda, necesitamos el useReducer y la API createContext de React, por eso lo importamos.
  • Importamos IState e IAction de ./types/interfaces .
  • Declaramos un objeto initialState con un tipo de IState y propiedades de episodios y favoritos, ambos configurados en una matriz vacía, respectivamente.
  • A continuación, creamos una variable Store que contiene el método createContext y se le pasa initialState .

El tipo de método createContext es <IState | any> <IState | any> , lo que significa que podría ser un tipo de <IState> o any . Veremos el tipo any usado a menudo en este artículo.

  • A continuación, declaramos una función reducer y pasamos state y action como parámetros. La función reducer tiene una declaración de cambio que verifica el valor de action.type . Si el valor es FETCH_DATA , devuelve un objeto que tiene una copia de nuestro estado (...state) y del estado del episodio que contiene nuestra carga útil de acción.
  • En la declaración de cambio, devolvemos un estado default .

Tenga en cuenta que los parámetros de state y action en la función reducer tienen tipos IState e IAction , respectivamente. Además, la función reducer tiene un tipo de IState .

  • Por último, declaramos una función StoreProvider . Esto le dará a todos los componentes de nuestra aplicación acceso a la tienda.
  • Esta función toma a children como apoyo, y dentro de la función StorePrivder , declaramos el gancho useReducer .
  • Desestructuramos state y dispatch .
  • Para que nuestra tienda sea accesible para todos los componentes, pasamos un valor de objeto que contiene state y dispatch .

El state que contiene nuestros episodios y el estado de favoritos será accesible a través de otros componentes, mientras que el dispatch es una función que cambia el estado.

  • Exportaremos Store y StoreProvider para que puedan usarse en nuestra aplicación.

Crear acción.ts

Tendremos que realizar solicitudes a la API para obtener los episodios que se mostrarán al usuario. Esto se hará en un archivo de acción. Cree un archivo Action.ts y luego pegue el siguiente código:

 import { Dispatch } from './interface/interfaces' export const fetchDataAction = async (dispatch: Dispatch) => { const URL = 'https://api.tvmaze.com/singlesearch/shows?q=la-casa-de-papel&embed=episodes' const data = await fetch(URL) const dataJSON = await data.json() return dispatch({ type: 'FETCH_DATA', payload: dataJSON.\_embedded.episodes }) }

Primero, necesitamos importar nuestras interfaces para que puedan usarse en este archivo. Se siguieron los siguientes pasos para crear la acción:

  • La función fetchDataAction toma props de dispatch como parámetro.
  • Debido a que nuestra función es asíncrona, async y await .
  • Creamos una variable ( URL ) que contiene nuestro punto final de API.
  • Tenemos otra variable llamada data que contiene la respuesta de la API.
  • Luego, almacenamos la respuesta JSON en dataJSON , después de haber obtenido la respuesta en formato JSON llamando a data.json() .
  • Por último, devolvemos una función de envío que tiene una propiedad de type y una cadena de FETCH_DATA . También tiene un payload() . _embedded.episodes es la matriz del objeto de episodios de nuestro endpoint .

Tenga en cuenta que la función fetchDataAction obtiene nuestro punto final, lo convierte en objetos JSON y devuelve la función de envío, que actualiza el estado declarado anteriormente en la Tienda.

El tipo de envío exportado se establece en React.Dispatch . Tenga en cuenta que React.Dispatch es el tipo estándar para la función de envío de acuerdo con la base de código @types/react , mientras que <IAction> es una matriz de la acción de la interfaz.

Componente EpisodesList

Para mantener la reutilización de nuestra aplicación, mantendremos todos los episodios obtenidos en un archivo separado y luego importaremos el archivo en nuestro componente de página de homePage .

En la carpeta de components , cree un archivo EpisodesList.tsx y copie y pegue el siguiente código:

 import React from 'react' import { IEpisode, IProps } from '../types/interfaces' const EpisodesList = (props: IProps): Array<JSX.Element> => { const { episodes } = props return episodes.map((episode: IEpisode) => { return ( <section key={episode.id} className='episode-box'> <img src={!!episode.image ? episode.image.medium : ''} alt={`Money Heist ${episode.name}`} /> <div>{episode.name}</div> <section style={{ display: 'flex', justifyContent: 'space-between' }}> <div> Season: {episode.season} Number: {episode.number} </div> <button type='button' > Fav </button> </section> </section> ) }) } export default EpisodesList
  • Importamos IEpisode e IProps desde interfaces.tsx .
  • A continuación, creamos una función EpisodesList que toma accesorios. Los accesorios tendrán un tipo de IProps , mientras que la función tiene un tipo de Array<JSX.Element> .

Visual Studio Code sugiere que nuestro tipo de función se escriba como JSX.Element[] .

Visual Studio Code sugiere un tipo (vista previa grande)

Mientras que Array<JSX.Element> es igual a JSX.Element[] , Array<JSX.Element> se denomina identidad genérica. Por lo tanto, el patrón genérico se usará con frecuencia en este artículo.

  • Dentro de la función, desestructuramos los episodes de props , que tiene el IEpisode como tipo.

Lea acerca de la identidad genérica. Este conocimiento será necesario a medida que avancemos.

  • Devolvimos los accesorios de los episodes y los mapeamos para devolver algunas etiquetas HTML.
  • La primera sección contiene la key , que es episode.id , y un nombre de className de episode-box , que se creará más adelante. Sabemos que nuestros episodios tienen imágenes; por lo tanto, la etiqueta de la imagen.
  • La imagen tiene un operador ternario que verifica si hay un episode.image o un episode.image.medium . De lo contrario, mostramos una cadena vacía si no se encuentra ninguna imagen. Además, incluimos el nombre del episode.name en un div.

En la section , mostramos la temporada a la que pertenece un episodio y su número. Tenemos un botón con el texto Fav . Hemos exportado el componente EpisodesList para que podamos usarlo en nuestra aplicación.

Componente de la página de inicio

Queremos que la página de inicio active la llamada API y muestre los episodios usando el componente EpisodesList que creamos. Dentro de la carpeta de components , cree el componente HomePage y copie y pegue el siguiente código:

 import React, { useContext, useEffect, lazy, Suspense } from 'react' import App from '../App' import { Store } from '../Store' import { IEpisodeProps } from '../types/interfaces' import { fetchDataAction } from '../Actions' const EpisodesList = lazy<any>(() => import('./EpisodesList')) const HomePage = (): JSX.Element => { const { state, dispatch } = useContext(Store) useEffect(() => { state.episodes.length === 0 && fetchDataAction(dispatch) }) const props: IEpisodeProps = { episodes: state.episodes, store: { state, dispatch } } return ( <App> <Suspense fallback={<div>loading...</div>}> <section className='episode-layout'> <EpisodesList {...props} /> </section> </Suspense> </App> ) } export default HomePage
  • Importamos useContext , useEffect , lazy y Suspense de React. El componente de la aplicación importada es la base sobre la cual todos los demás componentes deben recibir el valor de la tienda.
  • También importamos Store , IEpisodeProps y FetchDataAction desde sus respectivos archivos.
  • Importamos el componente EpisodesList usando la función React.lazy disponible en React 16.6.

La carga diferida de React es compatible con la convención de división de código. Por lo tanto, nuestro componente EpisodesList se carga dinámicamente, en lugar de cargarse de una vez, lo que mejora el rendimiento de nuestra aplicación.

  • Desestructuramos el state y lo dispatch como accesorios de la Store .
  • El ampersand (&&) en el gancho useEffect verifica si el estado de nuestros episodios está empty (o es igual a 0). De lo contrario, devolvemos la función fetchDataAction .
  • Por último, devolvemos el componente App . Dentro de él, usamos el envoltorio Suspense y establecemos el fallback en un div con el texto de loading . Esto se mostrará al usuario mientras esperamos la respuesta de la API.
  • El componente EpisodesList se montará cuando los datos estén disponibles, y los datos que contendrán los episodes son los que distribuimos en él.

Configurar Index.txs

El componente de la Homepage de inicio debe ser un elemento secundario del StoreProvider . Tendremos que hacer eso en el archivo index . Cambie el nombre de index.js a index.tsx y pegue el siguiente código:

 import React from 'react' import ReactDOM from 'react-dom' import './index.css' import { StoreProvider } from './Store' import HomePage from './components/HomePage' ReactDOM.render( <StoreProvider> <HomePage /> </StoreProvider>, document.getElementById('root') )

Importamos StoreProvider , HomePage e index.css desde sus respectivos archivos. We wrap the HomePage component in our StoreProvider . This makes it possible for the Homepage component to access the store, as we saw in the previous section.

Hemos recorrido un largo camino. Let's check what the app looks like, without any CSS.

App without CSS (Large preview)

Create Index.css

Delete the code in the index.css file and replace it with this:

 html { font-size: 14px; } body { margin: 0; padding: 0; font-size: 10px; font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", "Roboto", "Oxygen", "Ubuntu", "Cantarell", "Fira Sans", "Droid Sans", "Helvetica Neue", sans-serif; -webkit-font-smoothing: antialiased; -moz-osx-font-smoothing: grayscale; } .episode-layout { display: flex; flex-wrap: wrap; min-width: 100vh; } .episode-box { padding: .5rem; } .header { display: flex; justify-content: space-between; background: white; border-bottom: 1px solid black; padding: .5rem; position: sticky; top: 0; }

Our app now has a look and feel. Here's how it looks with CSS.

(Vista previa grande)

Now we see that our episodes can finally be fetched and displayed, because we've adopted TypeScript all the way. Great, isn't it?

Add Favorite Episodes Feature

Let's add functionality that adds favorite episodes and that links it to a separate page. Let's go back to our Store component and add a few lines of code:

Note that the highlighted code is newly added:

 import React, { useReducer, createContext } from 'react' import { IState, IAction } from './types/interfaces' const initialState: IState = { episodes: [], favourites: [] } export const Store = createContext<IState | any>(initialState) const reducer = (state: IState, action: IAction): IState => { switch (action.type) { case 'FETCH_DATA': return { ...state, episodes: action.payload }
 case 'ADD_FAV': return { ...state, favourites: [...state.favourites, action.payload] }
 default: return state } } export const StoreProvider = ({ children }: JSX.ElementChildrenAttribute): JSX.Element => { const [state, dispatch] = useReducer(reducer, initialState) return <Store.Provider value={{ state, dispatch }}>{children}</Store.Provider> }

To implement the “Add favorite” feature to our app, the ADD_FAV case is added. It returns an object that holds a copy of our previous state, as well as an array with a copy of the favorite state , with the payload .

We need an action that will be called each time a user clicks on the FAV button. Let's add the highlighted code to index.tx :

 import { IAction, IEpisode, Dispatch } from './types/interfaces'
export const fetchDataAction = async (dispatch: Dispatch) => { const URL = 'https://api.tvmaze.com/singlesearch/shows?q=la-casa-de-papel&embed=episodes' const data = await fetch(URL) const dataJSON = await data.json() return dispatch({ type: 'FETCH_DATA', payload: dataJSON._embedded.episodes }) }
export const toggleFavAction = (dispatch: any, episode: IEpisode | any): IAction => { let dispatchObj = { type: 'ADD_FAV', payload: episode } return dispatch(dispatchObj) }
export const toggleFavAction = (dispatch: any, episode: IEpisode | any): IAction => { let dispatchObj = { type: 'ADD_FAV', payload: episode } return dispatch(dispatchObj) }

We create a toggleFavAction function that takes dispatch and episodes as parameters, and any and IEpisode|any as their respective types, with IAction as our function type. We have an object whose type is ADD_FAV and that has episode as its payload. Lastly, we just return and dispatch the object.

Agregaremos algunos fragmentos más a EpisodeList.tsx . Copia y pega el código resaltado:

 import React from 'react' import { IEpisode, IProps } from '../types/interfaces' const EpisodesList = (props: IProps): Array<JSX.Element> => {
 const { episodes, toggleFavAction, favourites, store } = props const { state, dispatch } = store

 return episodes.map((episode: IEpisode) => { return ( <section key={episode.id} className='episode-box'> <img src={!!episode.image ? episode.image.medium : ''} alt={`Money Heist - ${episode.name}`} /> <div>{episode.name}</div> <section style={{ display: 'flex', justifyContent: 'space-between' }}> <div> Seasion: {episode.season} Number: {episode.number} </div> <button type='button'
 onClick={() => toggleFavAction(state, dispatch, episode)} > {favourites.find((fav: IEpisode) => fav.id === episode.id) ? 'Unfav' : 'Fav'}
 </button> </section> </section> ) }) } export default EpisodesList

Incluimos togglefavaction , favorites y store como accesorios, y desestructuramos state , un dispatch desde la tienda. Para seleccionar nuestro episodio favorito, incluimos el método toggleFavAction en un evento onClick y pasamos el state , el dispatch y los accesorios del episode como argumentos a la función.

Por último, recorremos el estado favorite para verificar si fav.id (ID favorito) coincide con el episode.id . Si es así, alternamos entre el texto Unfav y Fav . Esto ayuda al usuario a saber si ha marcado ese episodio como favorito o no.

Nos estamos acercando al final. Pero aún necesitamos una página a la que se puedan vincular los episodios favoritos cuando el usuario elija entre los episodios en la página de inicio.

Si has llegado hasta aquí, date una palmadita en la espalda.

Componente de página favorita

En la carpeta de components , cree un archivo FavPage.tsx . Copie y pegue el siguiente código en él:

 import React, { lazy, Suspense } from 'react' import App from '../App' import { Store } from '../Store' import { IEpisodeProps } from '../types/interfaces' import { toggleFavAction } from '../Actions' const EpisodesList = lazy<any>(() => import('./EpisodesList')) export default function FavPage(): JSX.Element { const { state, dispatch } = React.useContext(Store) const props: IEpisodeProps = { episodes: state.favourites, store: { state, dispatch }, toggleFavAction, favourites: state.favourites } return ( <App> <Suspense fallback={<div>loading...</div>}> <div className='episode-layout'> <EpisodesList {...props} /> </div> </Suspense> </App> ) }

Para crear la lógica detrás de la elección de episodios favoritos, hemos escrito un pequeño código. Importamos lazy y Suspense de React. También importamos Store , IEpisodeProps y toggleFavAction desde sus respectivos archivos.

Importamos nuestro componente EpisodesList utilizando la función React.lazy . Por último, devolvemos el componente App . Dentro de él, usamos el envoltorio Suspense y establecemos un respaldo a un div con el texto de carga.

Esto funciona de manera similar al componente de la Homepage de inicio. Este componente accederá a la tienda para obtener los episodios que el usuario haya marcado como favoritos. Luego, la lista de episodios se pasa al componente EpisodesList .

Agreguemos algunos fragmentos más al archivo HomePage.tsx .

Incluya el toggleFavAction de ../Actions . Incluya también el método toggleFavAction como accesorios.

 import React, { useContext, useEffect, lazy, Suspense } from 'react' import App from '../App' import { Store } from '../Store' import { IEpisodeProps } from '../types/interfaces'
import { fetchDataAction, toggleFavAction } from '../Actions'
const EpisodesList = lazy<any>(() => import('./EpisodesList')) const HomePage = (): JSX.Element => { const { state, dispatch } = useContext(Store) useEffect(() => { state.episodes.length === 0 && fetchDataAction(dispatch) }) const props: IEpisodeProps = { episodes: state.episodes, store: { state, dispatch },
 toggleFavAction, favourites: state.favourites
 } return ( <App> <Suspense fallback={<div>loading...</div>}> <section className='episode-layout'> <EpisodesList {...props} /> </section> </Suspense> </App> ) } export default HomePage

Nuestra FavPage debe estar vinculada, por lo que necesitamos un enlace en nuestro encabezado en App.tsx . Para lograr esto, usamos Reach Router, una biblioteca similar a React Router. William Le explica las diferencias entre Reach Router y React Router.

En su CLI, ejecute npm install @reach/router @types/reach__router . Estamos instalando tanto la biblioteca Reach Router como los tipos reach-router .

Tras una instalación exitosa, importe Link desde @reach/router .

 import React, { useContext, Fragment } from 'react' import { Store } from './tsx'
import { Link } from '@reach/router'
 const App = ({ children }: { children: JSX.Element }): JSX.Element => {
 const { state } = useContext(Store)
return ( <Fragment> <header className='header'> <div> <h1>Money Heist</h1> <p>Pick your favourite episode</p> </div>
 <div> <Link to='/'>Home</Link> <Link to='/faves'>Favourite(s): {state.favourites.length}</Link> </div>
 </header> {children} </Fragment> ) } export default App

Desestructuramos la tienda desde useContext . Por último, nuestra casa tendrá un Link y una ruta a / , mientras que nuestro favorito tendrá una ruta a /faves .

{state.favourites.length} comprueba el número de episodios en los estados favoritos y lo muestra.

Finalmente, en nuestro archivo index.tsx , importamos los componentes FavPage y HomePage , respectivamente, y los envolvemos en el Router .

Copie el código resaltado al código existente:

 import React from 'react' import ReactDOM from 'react-dom' import './index.css' import { StoreProvider } from './Store'
import { Router, RouteComponentProps } from '@reach/router' import HomePage from './components/HomePage' import FavPage from './components/FavPage' const RouterPage = ( props: { pageComponent: JSX.Element } & RouteComponentProps ) => props.pageComponent
ReactDOM.render( <StoreProvider>
 <Router> <RouterPage pageComponent={<HomePage />} path='/' /> <RouterPage pageComponent={<FavPage />} path='/faves' /> </Router>
 </StoreProvider>, document.getElementById('root') )

Ahora, veamos cómo funciona el ADD_FAV implementado.

El código "Agregar favorito" funciona (Vista previa grande)

Eliminar funcionalidad favorita

Finalmente, agregaremos la función "Eliminar episodio", para que cuando se haga clic en el botón, alterne entre agregar o eliminar un episodio favorito. Mostraremos la cantidad de episodios agregados o eliminados en el encabezado.

TIENDA

Para crear la funcionalidad "Eliminar episodio favorito", agregaremos otro caso en nuestra tienda. Entonces, vaya a Store.tsx y agregue el código resaltado:

 import React, { useReducer, createContext } from 'react' import { IState, IAction } from './types/interfaces' const initialState: IState = { episodes: [], favourites: [] } export const Store = createContext<IState | any>(initialState) const reducer = (state: IState, action: IAction): IState => { switch (action.type) { case 'FETCH_DATA': return { ...state, episodes: action.payload } case 'ADD_FAV': return { ...state, favourites: [...state.favourites, action.payload] }
 case 'REMOVE_FAV': return { ...state, favourites: action.payload }
 default: return state } } export const StoreProvider = ({ children }: JSX.ElementChildrenAttribute): JSX.Element => { const [state, dispatch] = useReducer(reducer, initialState) return {children} } default: return state } } export const StoreProvider = ({ children }: JSX.ElementChildrenAttribute): JSX.Element => { const [state, dispatch] = useReducer(reducer, initialState) return {children} } default: return state } } export const StoreProvider = ({ children }: JSX.ElementChildrenAttribute): JSX.Element => { const [state, dispatch] = useReducer(reducer, initialState) return {children} }

Agregamos otro caso llamado REMOVE_FAV y devolvemos un objeto que contiene la copia de nuestro initialState . Además, el estado de favorites contiene la carga útil de la acción.

ACCIÓN

Copie el siguiente código resaltado y péguelo en action.ts :

 import { IAction, IEpisode, IState, Dispatch } from './types/interfaces'
export const fetchDataAction = async (dispatch: Dispatch) => { const URL = 'https://api.tvmaze.com/singlesearch/shows?q=la-casa-de-papel&embed=episodes' const data = await fetch(URL) const dataJSON = await data.json() return dispatch({ type: 'FETCH_DATA', payload: dataJSON.\_embedded.episodes }) } //Add IState withits type
export const toggleFavAction = (state: IState, dispatch: any, episode: IEpisode | any): IAction => { const episodeInFav = state.favourites.includes(episode)
 let dispatchObj = { type: 'ADD_FAV', payload: episode }
 if (episodeInFav) { const favWithoutEpisode = state.favourites.filter( (fav: IEpisode) => fav.id !== episode.id ) dispatchObj = { type: 'REMOVE_FAV', payload: favWithoutEpisode }
 } return dispatch(dispatchObj) }

Importamos la interfaz IState desde ./types/interfaces , porque necesitaremos pasarla como el tipo a los accesorios de state en la función toggleFavAction .

Se episodeInFav una variable episodioInFav para comprobar si existe un episodio en el estado de favorites .

Filtramos a través del estado de favoritos para verificar si una identificación favorita no es igual a una identificación de episodio. Por lo tanto, a dispatchObj se le reasigna un tipo de REMOVE_FAV y una carga útil de favWithoutEpisode .

Veamos una vista previa del resultado de nuestra aplicación.

Conclusión

En este artículo, hemos visto cómo configurar TypeScript en un proyecto de React y cómo migrar un proyecto de Vanilla React a TypeScript.

También creamos una aplicación con TypeScript y React para ver cómo se usa TypeScript en los proyectos de React. Confío en que hayas podido aprender algunas cosas.

Comparta sus comentarios y experiencias con TypeScript en la sección de comentarios a continuación. ¡Me encantaría ver qué se te ocurre!

El repositorio de apoyo para este artículo está disponible en GitHub.

Referencias

  1. “Cómo migrar una aplicación React a TypeScript”, Joe Previte
  2. "¿Por qué y cómo usar TypeScript en su aplicación React?", Mahesh Haldar