El manejo de código asíncrono es un elemento básico en las aplicaciones JavaScript.
TypeScript aporta seguridad de tipos a las operaciones asíncronas, mejorando la predictibilidad y reduciendo los errores en tiempo de ejecución. Este artículo pretende explorar los patrones que podríamos aprovechar para gestionar las operaciones asíncronas y la gestión de errores de forma eficaz.
Async/Await: Código más claro
La sintaxis async/await
promueve un código más limpio y legible 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 comprueban en tiempo de compilación, reduciendo posibles errores en tiempo de ejecución.
async function fetchData(url: string): Promise<string> {
try {
const response = await fetch(url);
if (!response.ok) {
throw new Error(`Network response was not ok: ${response.statusText}`);
}
return await response.text();
} catch (error: unknown) {
// Error handling
// Preserving stack trace
throw error instanceof Error ? error : new Error("Unexpected error");
}
}
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 producirse.
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<string> = new Promise((resolve, reject) => {
const someCondition = true;
if (someCondition) {
resolve("Success!");
} else {
// TypeScript ensures this is an Error object
reject(new Error("Failure"));
}
});
Este ejemplo demuestra la capacidad de TypeScript para garantizar que se comprueba el tipo del objeto de error, lo que permite una gestión de errores precisa y resistente.
Genéricos mejorados y gestión de errores
Los genéricos en TypeScript mejoran la flexibilidad de las funciones a la vez 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: string
): Promise<Exclude<T, ErrorResponse>> {
const response: ContentResponse = await fetchContent(contentId);
if (response.kind === ResponseKind.Error) {
throw new Error(response.message);
}
// At this point, with ErrorResponse eliminated from the possible types due to our runtime check,
// we can be confident that the response is either an ArticleResponse or a CommentResponse.
// A more accurate TS Intellisense and a more predictable type (^o^)丿
return response as Exclude<T, ErrorResponse>;
}
// Usage of the getContent function with type assertion,
// reinforcing our expected return type for more predictable behavior and type safety.
async function displayContent(contentId: string) {
try {
// Here we assert that the response will be of type ArticleResponse.
const article = await getContent<ArticleResponse>(contentId);
// Type-safe access to the 'title' property.
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 apropiadamente.
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: number;
constructor(message: string, statusCode = 400) {
super(message);
this.name = "BadRequestError";
// Default to HTTP 400 status code for Bad Request
this.statusCode = statusCode;
}
}
type UserData = {
name: string;
};
async function submitUserData(userData: UserData): Promise<void> {
try {
validateUserData(userData);
// Handle data submission to an API...
} catch (error) {
if (error instanceof BadRequestError) {
console.error(`Validation failed: ${error.message}`);
// Handle bad request error
} else {
console.error(`Unexpected error: ${error.message}`);
// Handle unexpected errors
}
// Re-throw the error if you want to access the error from the higher-level calling function
throw error;
}
}
function validateUserData<T extends UserData>(data: T): void {
if (!data.name) {
throw new BadRequestError("Name is required");
}
// Some other validations...
}
Con clases de error personalizadas, podemos manejar excepciones de forma granular, complementando la seguridad de tipos en tiempo de compilación proporcionada por los genéricos.
Combinando 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 particularmente útil. El patrón de tipos Result
o Either
ofrece una alternativa estructurada a la gestión de errores tradicional.
Este enfoque trata los errores como datos, encapsulándolos dentro de 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<number> {
try {
// Asynchronous logic here...
const value = await someAsyncTask();
return { kind: 'success', value };
} catch (error) {
return {
kind: 'failure',
error: error instanceof Error ? error : new Error('Unknown error'),
};
}
}
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 forma 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?: string) => Error
): Promise<ErrorBoundary<T, Error>> {
try {
const data = await fn();
return { status: 'success', data };
} catch (error) {
const errorMessage = error instanceof Error ? error.message : 'Unknown error';
return {
status: 'error',
error: createError(errorMessage)
};
}
}
async function riskyAsyncOperation(): Promise<string> {
const someCondition = false;
if (someCondition) {
throw new Error('Failure');
}
return 'Success';
}
async function handleOperation() {
const result = await asyncHandleError(riskyAsyncOperation, (message) => new Error(message));
if (result.status === 'success') {
console.log(result.data); // Outputs 'Success'
} else {
console.error(result.error.message); // Outputs the error message, if any
}
}
// Execute the operation
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 forma estructurada y segura, promoviendo mejores 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, de nuevo, toma estos patrones, extiéndelos, y mira cómo pueden ayudarte a mejorar tu proyecto TypeScript en términos de mantenibilidad. Permanece atento a nuestro próximo tema; ¡nos vemos entonces!