«
La gestión de código asíncrono es un componente esencial en las aplicaciones JavaScript.
TypeScript aporta seguridad de tipos a las operaciones asíncronas, mejorando la previsibilidad y reduciendo los errores en tiempo de ejecución. Este artículo tiene como objetivo explorar los patrones que podríamos utilizar para manejar las operaciones asíncronas y la gestión de errores de manera efectiva.
Async/Await: Código más legible
La sintaxis async/await
fomenta un código más limpio y comprensible que se asemeja mucho a la ejecución síncrona.
La inferencia de tipos de TypeScript se alinea con esto, asegurando que las variables y los tipos de retorno se verifiquen en tiempo de compilación, reduciendo posibles errores en tiempo de ejecución.
async function fetchData(url: cadena): Promise<cadena> {
try {
const response = await fetch(url);
if (!response.ok) {
throw new Error(`La respuesta de la red no fue correcta: ${response.statusText}`);
}
return await response.text();
} catch (error: desconocido) {
// Manejo de errores
// Preservar la traza de la pila
throw error instanceof Error ? error : new Error("Error inesperado");
}
}
Promesas
Garantizando la seguridad de tipos en operaciones asíncronas
TypeScript mejora Promises reforzando los tipos tanto en el valor resuelto como en cualquier error que pueda ocurrir.
Esta comprobación de tipos en tiempo de compilación conduce a una salida más predecible en operaciones asíncronas, reduciendo significativamente el riesgo de errores inesperados en tiempo de ejecución.
const taskResult: Promise<cadena> = new Promise((resolve, reject) => {
const someCondition = true;
if (someCondition) {
resolve("¡Éxito!");
} else {
// TypeScript asegura que este sea un objeto de Error
reject(new Error("Fallo"));
}
});
Este ejemplo demuestra la capacidad de TypeScript para garantizar que se verifique el tipo del objeto de error, lo que permite una gestión precisa y resistente de errores.
Genéricos mejorados y gestión de errores
Los genéricos en TypeScript mejoran la flexibilidad de las funciones al mismo tiempo que mantienen la seguridad de tipos.
Considere una función asíncrona que obtiene diferentes tipos de contenido. Los genéricos permiten que esta función defina claramente su tipo de retorno, garantizando la seguridad de tipo en tiempo de compilación.
enum ResponseKind {
Article = "article",
Comment = "comment",
Error = "error",
}
type ArticleResponse = {
kind: ResponseKind.Article;
title: string;
content: string;
};
type CommentResponse = {
kind: ResponseKind.Comment;
content: string;
};
type ErrorResponse = {
kind: ResponseKind.Error;
message: string;
};
// Using a discriminated union to define response types
type ContentResponse = ArticleResponse | CommentResponse | ErrorResponse;
async function getContent<T extends ContentResponse>(
contentId: cadena
): Promise<Exclude<T, ErrorResponse>> {
const response: ContentResponse = await fetchContent(contentId);
if (response.kind === ResponseKind.Error) {
throw new Error(response.message);
}
// En este punto, con ErrorResponse eliminado de los tipos posibles debido a nuestra comprobación en tiempo de ejecución,
// podemos estar seguros de que la respuesta es ya sea un ArticleResponse o un CommentResponse.
// Una Intellisense de TS más precisa y un tipo más predecible (^o^)丿
return response as Exclude<T, ErrorResponse>;
}
// Uso de la función getContent con afirmación de tipo,
// reforzando nuestro tipo de retorno esperado para un comportamiento más predecible y seguro.
async function displayContent(contentId: cadena) {
try {
// Aquí afirmamos que la respuesta será de tipo ArticleResponse.
const article = await getContent<ArticleResponse>(contentId);
// Acceso seguro al propiedad 'title'.
console.log(article.title);
} catch (error) {
console.error(error);
}
}
La función getContent
anterior ilustra el uso de genéricos para lograr la seguridad de tipo en tiempo de compilación, asegurando que manejamos varios tipos de contenido adecuadamente.
Este enfoque reduce significativamente la probabilidad de errores en tiempo de ejecución.
Además, aprovechamos Exclude
para asegurar que getContent
no devuelve un ErrorResponse
, que es un ejemplo de cómo el sistema de tipos de TypeScript puede prevenir ciertas clases de errores en tiempo de ejecución por diseño.
A pesar de las robustas comprobaciones en tiempo de compilación de TypeScript, algunos errores son inherentemente en tiempo de ejecución y requieren un manejo explícito.
A continuación, echaremos un vistazo a cómo el manejo personalizado de errores puede actuar como una red de seguridad para aquellos errores que se escapan de las comprobaciones en tiempo de compilación.
Continuando con nuestras prácticas de obtención de datos de tipo seguro, es vital tener una estrategia robusta para los errores en tiempo de ejecución. La introducción de clases de error personalizadas a continuación proporciona un método detallado para diferenciar y manejar tales errores de manera efectiva.
class BadRequestError extends Error {
public statusCode: número;
constructor(message: cadena, statusCode = 400) {
super(message);
this.name = "BadRequestError";
// Valor por defecto para el código de estado HTTP 400 para Bad Request
this.statusCode = statusCode;
}
}
type UserData = {
name: cadena;
};
async function submitUserData(userData: UserData): Promise<nada> {
try {
validateUserData(userData);
// Manejar la entrega de datos a una API...
} catch (error) {
if (error instanceof BadRequestError) {
console.error(`La validación falló: ${error.message}`);
// Manejar el error de solicitud incorrecta
} else {
console.error(`Error inesperado: ${error.message}`);
// Manejar errores inesperados
}
// Volver a lanzar el error si se desea acceder al error desde la función de llamada de nivel superior
throw error;
}
}
function validateUserData<T extends UserData>(data: T): void {
if (!data.name) {
throw new BadRequestError("Se requiere un nombre");
}
// Algunas otras validaciones...
}
Con clases de error personalizadas, podemos manejar excepciones de manera detallada, complementando la seguridad de tipos en tiempo de compilación proporcionada por los genéricos.
Al combinar estas estrategias, creamos un sistema resistente que mantiene la seguridad de tipos tanto en tiempo de compilación como en tiempo de ejecución, proporcionando una red de seguridad completa para nuestras aplicaciones TypeScript.
Gestión alternativa de errores con tipos de resultado
Cuando se trata de operaciones asíncronas, un estilo de programación funcional puede ser especialmente útil. El patrón de tipos Result
o Either
ofrece una alternativa estructurada a la gestión de errores convencional.
Este enfoque trata los errores como datos, encapsulándolos en un tipo de resultado que puede propagarse fácilmente a través de flujos asíncronos.
type Success<T> = { kind: 'success', value: T };
type Failure<E> = { kind: 'failure', error: E };
type Result<T, E = Error> = Success<T> | Failure<E>;
type AsyncResult<T, E = Error> = Promise<Result<T, E>>;
async function asyncComplexOperation(): AsyncResult<número> {
try {
// Lógica asíncrona aquí...
const value = await someAsyncTask();
return { kind: 'success', value };
} catch (error) {
return {
kind: 'failure',
error: error instanceof Error ? error : new Error('Error desconocido'),
};
}
}
Gestión estructurada de errores en operaciones asíncronas
Para aplicaciones asíncronas más complejas, es posible que desee emplear tipos de límites de error para manejar los errores a un nivel superior.
Este patrón está diseñado para funcionar bien con la sintaxis async/await,
permitiendo que los errores se detecten y traten aguas arriba de manera limpia y predecible.
type ErrorBoundary<T, E extends Error> = {
status: 'success';
data: T;
} | {
status: 'error';
error: E;
};
async function asyncHandleError<T>(
fn: () => Promise<T>,
createError: (message?: cadena) => Error
): Promise<ErrorBoundary<T, Error>> {
try {
const data = await fn();
return { status: 'success', data };
} catch (error) {
const errorMessage = error instanceof Error ? error.message : 'Error desconocido';
return {
status: 'error',
error: createError(errorMessage)
};
}
}
async function riskyAsyncOperation(): Promise<cadena> {
const someCondition = false;
if (someCondition) {
throw new Error('Fallo');
}
return 'Éxito';
}
async function handleOperation() {
const result = await asyncHandleError(riskyAsyncOperation, (message) => new Error(message));
if (result.status === 'success') {
console.log(result.data); // Muestra 'Éxito'
} else {
console.error(result.error.message); // Muestra el mensaje de error, si hay alguno
}
}
// Ejecutar la operación
handleOperation();
En la adaptación asíncrona del patrón ErrorBoundary
, la función asyncHandleError
toma una función asíncrona y devuelve una promesa que resuelve un objeto de éxito o de error.
Esto garantiza que los errores asíncronos se gestionen de manera estructurada y segura, promoviendo buenas prácticas de gestión de errores en el código TypeScript.
Este artículo presenta estrategias para mejorar las operaciones asíncronas y la gestión de errores con TypeScript, mejorando la robustez y la mantenibilidad del código.
Hemos ilustrado estos patrones con ejemplos prácticos, subrayando su aplicabilidad en escenarios del mundo real. Así que, nuevamente, toma estos patrones, amplíalos y observa cómo pueden ayudarte a mejorar tu proyecto TypeScript en términos de mantenibilidad. Permanece atento a nuestro próximo tema; ¡nos vemos entonces!
»
En Grupo MET podemos ayudarte a implementar esta y muchas mas herramienta para optimizar tu trabajo. ¡Contáctanos para saber más!