Inicio / Artículos

Sindicatos discriminados en TypeScript: hacer que los estados ilegales no sean representables

Sustituya los conjuntos de estados booleanos por uniones discriminadas de TypeScript: exhaustividad, tipos Result, cargas de archivos, autenticación, proceso de pago y propiedades que no pueden ser almacenadas incorrectamente.

Editor: REACTAPP.TOP16 min de lectura3622 palabras

Fuentes: Discriminated Unions: The TypeScript Feature That Makes Bugs Impossible

A veces, la vista del perfil muestra simultáneamente un spinner en proceso y una bandera de error de color carmesí.

Nadie implementó intencionalmente un componente SpinnerWithError. La estructura del tipo de estado permitió silenciosamente esa combinación, y una conexión inestable la hizo visible.

Este artículo trata sobre el hábito de modelado que permite dichas pantallas, así como sobre la herramienta de TypeScript que las excluye del conjunto representable: la unión discriminada (también conocida como unión etiquetada o tipo de datos algebraico en otros lenguajes).

Un recordatorio útil del manual de TypeScript: un campo compartido con tipos literales permite al verificador determinar qué miembro de la unión es verdadero en ese momento.

Parte 1: El problema

Un tipo, muchas formas

Casi todos los repositorios de código de React han tenido un conjunto de estados similar a este:

interface UserState {
  isLoading: boolean;
  user?: User;
  error?: Error;
}

Cuente las combinaciones. isLoading puede ser verdadero o falso; user está presente o ausente; error está presente o ausente. Eso da ocho formas expresables. El producto solo necesita unas tres:

  1. Cargando, sin usuario, sin error
  2. Finalizado, usuario presente, sin error
  3. Finalizado, sin usuario, hay error

Las otras cinco no tienen sentido: cargar con un error, éxito y error al mismo tiempo, finalizado sin nada en absoluto, etc.

La analogía del restaurante

Imagínese un ticket de pedido universal donde cada campo es opcional: número de mesa, hora de recogida, dirección de entrega, nombre del mensajero. El personal suele completarlo correctamente por costumbre. El formulario sigue permitiendo “mesa 7” y “entregar a 12 Oak Street” en el mismo formulario.

Las propiedades opcionales modelan la ausencia: este campo puede quedar en blanco—cuando se necesitan alternativas: o bien se come en el local o se hace entrega, nunca ambas cosas.

Cómo aparece el olor

Puedes detectarlo sin necesidad de leer las reglas del negocio.

Afirmaciones que no pueden ser nulas. La verificación estaba tres líneas más arriba; el compilador no la siguió.

if (!state.isLoading && !state.error) {
  return <Profile user={state.user!} />; // "trust me"
}

Cada ! representa una promesa que no se aplica. Permanece vigente hasta que alguien edite la condición correspondiente.

Ramas que nunca se ejecutarán.

if (state.user) {
  return <Profile user={state.user} />;
} else {
  return <p>Something went wrong</p>; // dead code you still had to write, and now have to maintain
}

Inflación booleana. Los requisitos aumentan; alguien agrega otra bandera y luego otra más.

interface UploadState {
  isUploading: boolean;
  isProcessing: boolean;
  isComplete: boolean;
  hasFailed: boolean;
  isCancelled: boolean;
}

Cinco valores booleanos significan treinta y dos combinaciones para aproximadamente cinco modos reales. Es como una habitación de hotel con cinco interruptores sin etiquetar: puedes activarlos todos, pero la mayoría no sirven para nada útil.

Reglas que solo existen en la cabeza de las personas. “Primero verificar el error, luego al usuario, y finalmente la carga”. Cierto en el primer caso; opcional en todos los demás.

Bugues que aparecen en redes deficientes. Un fallo activa error. Un intento de repetición activa user. Nadie elimina error, porque hacerlo es un paso separado del que nadie se hace responsable.

El conjunto booleano es atractivo porque se relaciona de manera clara con los campos de formulario y las actualizaciones al estilo Redux. El costo es combinatorio: cada nueva bandera multiplica el espacio de valores inválidos. Las uniones discriminadas invierten ese equilibrio: agregar un estado añade una alternativa con nombre en lugar de duplicar la estructura.

Parte 2: La solución

Describa los estados, no los campos

Modele la misma funcionalidad como alternativas:

type UserState =
  | { status: 'loading' }
  | { status: 'success'; user: User }
  | { status: 'error'; error: Error };

Tres estados: exactamente tres. { status: 'loading', error: e } genera un error de compilación. user no existe hasta que la rama de éxito lo demuestre.

Volviendo al restaurante: imprima tres tickets etiquetados como DINE-IN, TAKEAWAY y DELIVERY. Cada ticket solo contendrá los campos que ese tipo de ticket requiere. Esa etiqueta es el discriminante: una propiedad de tipo literal de cadena, no string.

Tres elementos hacen que este patrón funcione:

  1. Una unión de tipos de objeto (las alternativas)
  2. Una propiedad compartida con tipos literales (la etiqueta)
  3. La verificación de dicha propiedad para que TypeScript pueda seleccionar el miembro activo

El punto 3 se refiere al narrowing:

function render(state: UserState) {
  switch (state.status) {
    case 'loading':
      return <Spinner />;
    case 'success':
      return <Profile user={state.user} />;   // user is User. Not User | undefined.
    case 'error':
      return <ErrorBanner error={state.error} />; // and here, user doesn't exist at all
  }
}

No !. No ramas muertas. Dentro de cada case, state tiene un tipo diferente deducido a partir de una comparación de cadenas.

La parte que paga el alquiler

Reducir las opciones resulta agradable. La exhaustividad es la razón por la que este patrón se mantiene durante años.

Agrega un pequeño ayudante:

function assertNever(value: never): never {
  throw new Error(`Unhandled variant: ${JSON.stringify(value)}`);
}

Conéctalo a default:

switch (state.status) {
  case 'loading': return <Spinner />;
  case 'success': return <Profile user={state.user} />;
  case 'error':   return <ErrorBanner error={state.error} />;
  default:        return assertNever(state);
}

never no tiene habitantes. Después de tratar cada caso, state se ha reducido a nada, por lo que assertNever solo verifica el tipo cuando el switch está completo.

Cuando el producto solicita un estado sin iniciar sesión:

type UserState =
  | { status: 'loading' }
  | { status: 'success'; user: User }
  | { status: 'error'; error: Error }
  | { status: 'unauthenticated'; loginUrl: string };  // new

Cada switch que ignora al nuevo miembro deja de compilar, indicando el archivo y la línea específicos, en lugar de señalar que “podría comportarse incorrectamente”.

Imagínese una máquina expendedora que recibe una moneda nueva. Una máquina deficiente la traga en silencio, mientras que una buena la rechaza en cada ranura para la cual no se haya programado esa nueva denominación.

¿Prefiere el error en el propio switch? Anote el tipo de retorno (function render(state: UserState): ReactNode). Si se omite un caso, la firma de la función fallará sin necesidad de usar assertNever.

El principio subyacente

Analice cada tipo con esta pregunta:

¿Cuántos “habitantes” de este tipo describen situaciones que en realidad no pueden ocurrir?

Cada valor ilegal representa un posible error futuro, una verificación defensiva y una prueba que nadie escribe. Trate de reducir esa cantidad a cero: haga que los estados ilegales no sean representables.

Parte 3: Donde esto da sus frutos en código real

No se trata de formas teóricas: es aquí donde este patrón demuestra su utilidad real.

1. Carga de datos (incluidos los estados que todos olvidan)

Hágalo correctamente. A menudo se pasan por alto dos estados: la actualización mientras los datos obsoletos siguen visibles, y el fallo sin descartar lo que ya se tiene.

type RemoteData<T, E = ApiError> =
  | { status: 'idle' }
  | { status: 'loading' }
  | { status: 'refreshing'; data: T }              // stale-while-revalidate
  | { status: 'success'; data: T; fetchedAt: number }
  | { status: 'failed'; error: E; lastData?: T };  // failed, but the page still has content

Las acciones que lo impulsan también forman una unión discriminada, al igual que cada reducer que haya escrito:

type Action<T, E> =
  | { type: 'fetch' }
  | { type: 'resolved'; data: T }
  | { type: 'rejected'; error: E };

function reducer<T, E>(state: RemoteData<T, E>, action: Action<T, E>): RemoteData<T, E> {
  switch (action.type) {
    case 'fetch':
      return 'data' in state
        ? { status: 'refreshing', data: state.data }
        : { status: 'loading' };
    case 'resolved':
      return { status: 'success', data: action.data, fetchedAt: Date.now() };
    case 'rejected':
      return { status: 'failed', error: action.error, lastData: 'data' in state ? state.data : undefined };
    default:
      return assertNever(action);
  }
}

useReducer, Redux Toolkit, XState, event sourcing: el mismo patrón bajo diferentes nombres.

2. Errores con los que realmente se puede actuar

Las excepciones lanzadas son invisibles para los tipos. Una función que lanza excepciones parece una tranquila; catch (e) devuelve unknown; quienes la llaman no pueden ver los modos de fallo.

Un tipo Result es como una nota del mensajero que explica qué sucedió, en lugar de irse y quizás informar más tarde al depósito:

type Result<T, E> = { ok: true; value: T } | { ok: false; error: E };

Los literales booleanos funcionan como discriminantes y se leen con claridad aquí.

La ventaja mayor es que se detectan los fallos de tipo en lugar de un simple Error:

type FetchFailure =
  | { kind: 'offline' }
  | { kind: 'timeout'; afterMs: number }
  | { kind: 'http'; status: number; body: unknown }
  | { kind: 'invalid_response'; issues: string[] }
  | { kind: 'aborted' };

async function getInvoice(id: string): Promise<Result<Invoice, FetchFailure>> { /* ... */ }

La interfaz de usuario puede responder de manera específica, y el compilador insiste en que se tenga en cuenta cada tipo:

const result = await getInvoice(id);

if (!result.ok) {
  switch (result.error.kind) {
    case 'offline':          return toast('You seem to be offline.');
    case 'timeout':          return toast(`The server took over ${result.error.afterMs}ms. Try again?`);
    case 'http':             return result.error.status === 404 ? notFound() : serverError();
    case 'invalid_response': return reportToSentry(result.error.issues);
    case 'aborted':          return; // user navigated away; say nothing
    default:                 return assertNever(result.error);
  }
}

renderInvoice(result.value); // narrowed to the success branch

La alternativa es catch (e) { toast('Something went wrong') }: lo que escriben los conjuntos de código cuando los tipos no son relevantes.

3. Cargas, donde cada estado cuenta con sus propios botones

Los miembros de una unión pueden contener diferentes funciones, no solo datos; por lo tanto, una operación puede estar indisponible en algunos estados por diseño:

type Upload =
  | { status: 'queued';    file: File; cancel(): void }
  | { status: 'uploading'; file: File; progress: number; cancel(): void }
  | { status: 'scanning';  file: File }                          // too late to cancel
  | { status: 'done';      file: File; url: string; remove(): Promise<void> }
  | { status: 'failed';    file: File; reason: string; retry(): void };

No se puede cancelar una pizza que ya está en el horno, así que la opción de cancelación no existe para ese caso. upload.retry() no se compilará hasta que se seleccione un estado que incluya la función retry.

4. Autenticación y la aparición de la interfaz incorrecta

null agrupaba dos situaciones diferentes: todavía no lo sabemos y nadie está conectado.

type Session =
  | { status: 'initializing' }                                          // ≠ logged out
  | { status: 'anonymous' }
  | { status: 'active';    user: User; accessToken: string; expiresAt: number }
  | { status: 'expiring';  user: User; refresh(): Promise<void> }
  | { status: 'locked';    user: User; reason: 'mfa_required' | 'password_reset' };

Si se separan, el flash incorrecto se vuelve inescrutable.

5. Flujos de múltiples pasos, donde el tipo recuerda

En el proceso de pago, cada paso necesita lo que produjeron los pasos anteriores. Un único objeto grande con todo como opcional obliga a realizar maniobras complejas con address!.

Deje que cada paso contenga solo lo que sabe:

type Checkout =
  | { step: 'cart';      items: CartItem[] }
  | { step: 'shipping';  items: CartItem[]; address: Address }
  | { step: 'payment';   items: CartItem[]; address: Address; shipping: ShippingOption }
  | { step: 'review';    items: CartItem[]; address: Address; shipping: ShippingOption; payment: PaymentMethod }
  | { step: 'confirmed'; orderId: string; total: Money; receiptUrl: string };

Es estructuralmente imposible llegar a la pantalla de pago sin una dirección. La pantalla de pago acepta un único miembro de la unión como parámetros:

type PaymentStepProps = Extract<Checkout, { step: 'payment' }>;

Extract obtiene un miembro por etiqueta; esta se vuelve constante una vez que los componentes están definidos de esta manera.

6. Elementos que comparten un nombre y nada más

Los métodos de pago comparten casi ningún campo. Una sola interfaz genera quince opciones y validaciones repletas de if (type === ...).

type PaymentMethod =
  | { type: 'card';       brand: 'visa' | 'mastercard' | 'amex'; last4: string; expMonth: number; expYear: number }
  | { type: 'paypal';     email: string }
  | { type: 'sepa_debit'; iban: string; mandateId: string }
  | { type: 'apple_pay';  deviceAccount: string }
  | { type: 'invoice';    companyName: string; vatId: string; netDays: 14 | 30 | 60 };

function describe(method: PaymentMethod): string {
  switch (method.type) {
    case 'card':       return `${method.brand.toUpperCase()} •••• ${method.last4}`;
    case 'paypal':     return `PayPal (${method.email})`;
    case 'sepa_debit': return `SEPA •••• ${method.iban.slice(-4)}`;
    case 'apple_pay':  return 'Apple Pay';
    case 'invoice':    return `Invoice, net ${method.netDays} days`;
    default:           return assertNever(method);
  }
}

Cuando llega Klarna, una versión muestra conjuntamente el mapa de íconos, el generador de recibos, la ruta de reembolso y la tabla de administración.

7. Mensajes del mundo exterior

Sockets, SSE, postMessage, extensiones: los datos transmitidos son desconocidos hasta que se demuestre lo contrario:

type ServerMessage =
  | { type: 'presence.join';  userId: string; at: number }
  | { type: 'presence.leave'; userId: string; at: number }
  | { type: 'cursor.move';    userId: string; x: number; y: number }
  | { type: 'doc.patch';      revision: number; ops: Op[] }
  | { type: 'doc.conflict';   serverRevision: number; snapshot: Doc }
  | { type: 'error';          code: 'RATE_LIMITED' | 'UNAUTHORIZED' | 'STALE_REVISION'; retryAfterMs?: number };

Disciplina: validar una vez en la frontera y luego confiar en el interior. Control de pasaportes, no verificaciones de identificación en cada pasillo. El asistente discriminador de Zod es adecuado para esto:

const ServerMessage = z.discriminatedUnion('type', [
  z.object({ type: z.literal('presence.join'), userId: z.string(), at: z.number() }),
  // ...
]);

socket.onmessage = (e) => {
  const parsed = ServerMessage.safeParse(JSON.parse(e.data));
  if (!parsed.success) return reportProtocolViolation(parsed.error);
  handle(parsed.data); // fully typed from here on
};

8. Documentos y estructuras jerárquicas

Las uniones pueden ser recursivas: es la forma natural para texto enriquecido, generadores de páginas, definiciones de formularios y árboles de expresiones consultadas:

type Block =
  | { type: 'paragraph'; children: Inline[] }
  | { type: 'heading';   level: 1 | 2 | 3; children: Inline[] }
  | { type: 'list';      ordered: boolean; items: Block[][] }
  | { type: 'image';     src: string; alt: string; caption?: string }
  | { type: 'code';      language: string; source: string };

type Inline =
  | { type: 'text';    value: string; marks?: ('bold' | 'italic' | 'code')[] }
  | { type: 'link';    href: string; children: Inline[] }
  | { type: 'mention'; userId: string };

function renderBlock(block: Block): ReactNode {
  switch (block.type) {
    case 'paragraph': return <p>{block.children.map(renderInline)}</p>;
    case 'heading':   return createElement(`h${block.level}`, null, block.children.map(renderInline));
    case 'list':      return block.ordered ? <ol>{/* ... */}</ol> : <ul>{/* ... */}</ul>;
    case 'image':     return <figure><img src={block.src} alt={block.alt} /></figure>;
    case 'code':      return <Highlight lang={block.language}>{block.source}</Highlight>;
    default:          return assertNever(block);
  }
}

Agregue un bloque table y el comprobador incluirá al renderizador, serializador, extractor de texto plano, contador de palabras e índice de búsqueda.

9. Propiedades de componentes que no pueden interpretarse incorrectamente

Los sindicatos expresan: “si se aprueba esto, debe aprobarse aquello y no se puede aprobar lo otro”:

type ButtonProps = { children: ReactNode; size?: 'sm' | 'md' | 'lg' } & (
  | { as: 'button'; onClick: () => void; type?: 'button' | 'submit' }
  | { as: 'anchor'; href: string; target?: '_blank'; download?: boolean }
);
<Button as="anchor" onClick={fn} />   // bad: anchors don't take onClick
<Button as="anchor" />                // bad: href is required
<Button as="button" onClick={fn} />   // good

La misma idea sirve para eliminar errores conocidos:

// No more "a component is changing an uncontrolled input to be controlled"
type InputProps =
  | { mode: 'controlled';   value: string; onChange: (v: string) => void }
  | { mode: 'uncontrolled'; defaultValue?: string };

// An icon-only button must have a label; a text button must not repeat one
type IconButtonProps =
  | { icon: IconName; 'aria-label': string; children?: never }
  | { icon?: IconName; children: ReactNode };

La segunda aplica una regla de accesibilidad en tiempo de compilación (children?: never prohíbe un atributo en una rama).

Y una docena más, brevemente

Una vez que se adopta esta idea, la se observa en objetos de ruta ({ name: 'order'; orderId: string } en lugar de cadenas de rutas concatenadas), enums de marcadores, variantes de notificaciones, tipos de campos de formulario, concesiones de permisos, fases de animación y catálogos similares.

Parte 4: Unir más técnicas

El restringimiento no se limita solo al switch.

if (state.status === 'success') { /* narrowed */ }
const label = state.status === 'error' ? state.error.message : '';

// Destructuring narrows the sibling variables too (TS 4.6+)
function handle({ kind, payload }: { kind: 'add'; payload: number } | { kind: 'set'; payload: string }) {
  if (kind === 'add') return payload.toFixed(2);   // payload is number
  return payload.toUpperCase();                     // payload is string
}

Eso funciona para parámetros y enlaces const. Pero obtener un objeto rest no funciona así: const { kind, ...rest } = shape deja a rest demasiado amplio.

// switch (true) narrows as well (TS 5.3+) — handy for mixed conditions
function view(state: RemoteData<Report>) {
  switch (true) {
    case state.status === 'success' && isStale(state.fetchedAt):
      return <Stale data={state.data} />;
    case state.status === 'success':
      return <Fresh data={state.data} />;
    default:
      return <Spinner />;
  }
}

Un objeto de búsqueda en lugar de un switch. A menudo es más claro y sigue siendo exhaustivo: si se omite una clave, no se compila.

type Handlers = {
  [K in ServerMessage['type']]: (msg: Extract<ServerMessage, { type: K }>) => void;
};

const handlers: Handlers = {
  'presence.join':  (m) => addUser(m.userId),
  'presence.leave': (m) => removeUser(m.userId),
  'cursor.move':    (m) => moveCursor(m.userId, m.x, m.y),
  'doc.patch':      (m) => applyOps(m.ops),
  'doc.conflict':   (m) => reconcile(m.snapshot),
  'error':          (m) => showError(m.code),
  // all six keys are mandatory — that's the point
};

Hay un problema. La llamada obvia no realiza verificación de tipos.

handlers[msg.type](msg); // bad: Argument of type 'ServerMessage' is not assignable to parameter of type 'never'

TypeScript trata handlers[msg.type] como una unión de funciones y exige un argumento aceptable para todas ellas, es decir, la intersección, que está vacía cuando los mensajes tienen formas distintas.

Localice una conversión en el despachador:

// Localise one cast at the dispatcher
function dispatch(msg: ServerMessage) {
  (handlers[msg.type] as (m: ServerMessage) => void)(msg);
}

// Or don't build a dispatcher at all — call the handlers from a switch, no casts anywhere
switch (msg.type) {
  case 'presence.join': return handlers['presence.join'](msg);
  case 'cursor.move':   return handlers['cursor.move'](msg);
  // ...
}

Tenga esto en cuenta antes de refactorizar un switch funcional en un mapa.

Constructores pequeños mantienen ordenados los puntos de llamada:

const ok  = <T,>(value: T) => ({ ok: true,  value }) as const;
const err = <E,>(error: E) => ({ ok: false, error }) as const;

Varias etiquetas se componen:

type Notification =
  | { channel: 'email'; kind: 'digest';        frequency: 'daily' | 'weekly' }
  | { channel: 'email'; kind: 'transactional'; templateId: string }
  | { channel: 'push';  kind: 'transactional'; deviceToken: string };

if (n.channel === 'email' && n.kind === 'digest') { /* down to one member */ }

Parte 5: Cosas que vale la pena recordar

La etiqueta debe ser un tipo literal. Este es el principal obstáculo; el mensaje de error confunde a los principiantes.

const action = { type: 'fetch' };            // inferred as { type: string } — not assignable
const action = { type: 'fetch' } as const;   // ok
const action: Action = { type: 'fetch' };    // ok. an annotation also prevents widening

Preferir type en lugar de interface. Un interface no puede ser una unión. Aquí la elección es importante.

El acotamiento puede desaparecer dentro de las callbacks. Sí sobrevive en una función anidada cuando el vínculo es un parámetro const que nunca se vuelve a asignar:

function ok(state: UserState) {
  if (state.status === 'success') {
    setTimeout(() => console.log(state.user), 0);   // fine, state is never reassigned
  }
}

Dos patrones anulan esta garantía. Asignar un nuevo valor al vínculo en cualquier parte de la función contenedora, incluso después de la callback anidada, elimina el acotamiento para todo el cuerpo de la función:

function broken(input: UserState, other: UserState) {
  let state = input;
  if (state.status === 'success') {
    setTimeout(() => console.log(state.user), 0);   // user does not exist on all members
  }
  state = other;                                     // this line is why
}

También el acotamiento a nivel de propiedad se detiene en las funciones anidadas: el verificador no puede demostrar que el objeto no fue modificado en ese intervalo:

if (box.state.status === 'success') {
  setTimeout(() => console.log(box.state.user), 0); // bad
}

En ambos casos, copie los campos necesarios a una variable local const antes de programar el trabajo:

if (box.state.status === 'success') {
  const { user } = box.state;
  setTimeout(() => console.log(user), 0);           // good
}

Depende del discriminante en lugar de de las comprobaciones con in. Una prueba tipo 'data' in state puede funcionar, pero solo es una suposición estructural y falla cuando dos variantes comparten el mismo nombre de propiedad. Un tag explícito deja claro la intención.

Estandariza los nombres de los tags. Utiliza un mismo vocabulario en todo el código: por ejemplo, type para eventos, status para ciclos de vida asíncronos y kind para variantes de error, de modo que herramientas como Extract sigan siendo fáciles de leer. Mezclar etiquetas entre diferentes dominios dificulta la comprensión de dichas herramientas.

Omite el tag cuando no aporta nada. Cuando las variantes comparten los mismos campos y solo difieren por una etiqueta, basta con una unión simple de cadenas; por ejemplo, type Size = 'sm' | 'md' | 'lg'—sin necesidad de envolver cada etiqueta en su propio objeto.

Observe el costo en tiempo de compilación. Las uniones extremadamente grandes, especialmente después de mapearlas o interseccionarlas, pueden hacer que el verificador de tipos funcione notablemente más lento. Diseña el modelo según el dominio del producto antes de que la lentitud del editor se convierta en un problema constante.

Los tipos no validan la entrada. La operación de conversión JSON.parse(x) as ServerMessage nunca inspecciona el contenido del mensaje. Realiza la解析 y la validación en los límites del sistema, y luego permite que el código interno confíe en las formas de datos simplificadas.

Antes de adoptar este patrón en todas partes, recuerda también que la documentación y la telemetría en tiempo de ejecución siguen siendo importantes: los tipos impiden estados ilegales en el editor, pero los operadores aún necesitan registros cuando un límite de red rechaza un mensaje.

Cuándo utilizarlo

Preferir una unión con etiquetas cuando la codificación diaria presente estos problemas:

  • una interfaz contiene campos que solo tienen sentido como grupo, no de forma independiente
  • Otro valor booleano se aplica a un objeto que ya cuenta con una bandera activada.
  • Las afirmaciones o conversiones que no devuelven null se utilizan únicamente para tranquilizar al verificador respecto a un campo.
  • Los comentarios explican qué combinaciones son válidas porque el tipo en sí no lo permite.
  • La capacitación incluye una lista verbal de las banderas que pueden coexistir.
  • La migración suele ser mecánica: se enumeran los estados reales en los que se encuentra el sistema, se nombra cada uno de ellos, se traslada cada campo a los estados donde existe y luego se siguen los errores de compilación. Se debe revisar esa lista de errores con detenimiento, ya que indica todos los lugares donde estaba el tipo anterior; casi siempre es más larga de lo esperado. Una vez que esos cambios se compilan nuevamente, la captura de pantalla con el indicador en rotación y errores deja de representar un estado de interfaz usable.

    Lecturas relacionadas

  • TypeScript no es lento: lo son los tipos sobre-diseñados — Las soluciones genéricas, la aplicación prematura del principio DRY a los tipos, el uso de flags opcionales y las trucos a nivel de tipo reducen la velocidad. Prefiera interfaces simples, uniones discriminadas y un uso medido de las herramientas de TypeScript.
  • Convenios de TypeScript que mantienen el código estricto y flexible — Opiniones prácticas sobre TypeScript: modo estricto, el uso de unknown sobre cualquier tipo, afirmaciones escasas, uniones de cadenas, diferencia entre tipos y interfaces, atributos JSDoc, utilidades y uniones discriminadas.