Saltar al contenido

Tipar el contexto: valor, default nullable y narrowing

Tipar bien un contexto en TypeScript es codificar su contrato: el tipo del valor —a menudo accesores o un store, no datos planos—, el default nullable que las sobrecargas de createContext imponen cuando no hay valor por defecto, y el estrechamiento en el hook useX que, al lanzar ante undefined, devuelve el tipo no nulo que los consumidores usan sin comprobar.

El tipo del valor: describe algo reactivo

El primer error de tipado es describir el contexto como si guardara datos planos. Un contexto de tema no lleva un string; lleva una forma reactiva: un accesor al tema y una acción para cambiarlo. El tipo debe reflejarlo.

TSX
import { type Accessor } from "solid-js";type Tema = "claro" | "oscuro";interface TemaContextValor {  tema: Accessor<Tema>; // un getter reactivo, no un Tema suelto  alternar: () => void; // una accion que muta la fuente}

Que el campo sea Accessor<Tema> —una función () => Tema— y no Tema es toda la diferencia: comunica que el consumidor debe llamarlo para leer y suscribirse, y hace imposible pasar por error un valor congelado. Cuando el estado es más rico, el tipo suele ser una tupla [estado, acciones] con un store, que tiparemos al final. La regla: el tipo del contexto es el tipo de su interfaz reactiva, no el de sus datos.

Entre una interfaz de campos nombrados y una tupla [estado, acciones] la elección es de estilo, con un matiz práctico: la tupla permite renombrar al desestructurar —const [carrito, acciones] = useCarrito()—, cómodo cuando varios contextos exponen un estado; la interfaz gana en legibilidad cuando hay muchos campos y el orden posicional cansa. Ambas tipan con el mismo rigor; lo que no vale es un valor plano sin envoltura reactiva.

Las dos sobrecargas de createContext

createContext tiene dos firmas, y elegir entre ellas es una decisión de diseño con consecuencias en el tipo.

TSX
// Sin valor por defecto: el tipo del contexto incluye undefinedconst TemaContext = createContext<TemaContextValor>();//    tipo: Context<TemaContextValor | undefined>// Con valor por defecto: el tipo NO incluye undefinedconst TemaContext2 = createContext<TemaContextValor>(valorPorDefecto);//    tipo: Context<TemaContextValor>

Sin argumento, la firma es createContext<T>(): Context<T | undefined>: el compilador añade undefined al tipo porque, sin default, un consumidor sin Provider recibirá justo eso. Con argumento, createContext<T>(def: T): Context<T>: al haber un fondo garantizado, el tipo es limpio. El tipo, por sí solo, te dice si el Provider es obligatorio: si ves | undefined, alguien tendrá que lidiar con su ausencia.

El narrowing en el hook: de nullable a garantizado

El tipo nullable es honesto pero incómodo: nadie quiere escribir sesion?.usuario en cien sitios. El patrón useX resuelve el runtime y el tipo de una vez. Al lanzar cuando el valor es undefined, TypeScript estrecha el tipo de retorno: después del throw, el control solo continúa si el valor existe, así que el compilador elimina undefined del tipo devuelto.

TSX
const SesionContext = createContext<SesionValor>(); // SesionValor | undefinedexport function useSesion(): SesionValor {  const ctx = useContext(SesionContext); // SesionValor | undefined  if (!ctx) {    throw new Error("useSesion requiere un SesionProvider por encima");  }  return ctx; // aqui el tipo ya es SesionValor, sin undefined}

El if (!ctx) throw es un control flow narrowing clásico: al no retornar en esa rama, TypeScript deduce que en la línea siguiente ctx no puede ser undefined. Anotar el retorno como SesionValor documenta y verifica el contrato. Los consumidores reciben un tipo limpio y acceden a sus campos sin encadenar interrogaciones.

Existe la variante con funciones de aserción —una firma del tipo asserts ctx is SesionValor— pero para el caso del contexto el hook que estrecha por throw es más simple y directo: no necesitas un predicado aparte cuando el propio hook es el cuello de botella por el que todos pasan.

Tipar el valor reactivo: accesores y stores

El caso más frecuente en producción mete un store en el contexto y expone la tupla [estado, acciones]. Tiparla con precisión aprovecha los tipos que Solid exporta.

TSX
import { type Store } from "solid-js/store";interface Linea { id: string; precio: number; cantidad: number }interface Carrito { lineas: Linea[] }// El estado es de solo lectura para el consumidor; las acciones lo mutantype CarritoValor = [  estado: Store<Carrito>,  acciones: {    agregar: (l: Linea) => void;    vaciar: () => void;  },];const CarritoContext = createContext<CarritoValor>();

Exponer Store<Carrito> —no SetStoreFunction— a los consumidores comunica que el estado es de solo lectura para ellos: mutar es privilegio de las acciones, que encapsulan el setStore. Es la misma disciplina de un reducer: el estado se lee, las acciones lo cambian, y el tipo lo hace cumplir. Cuando construyas el Provider que puebla este contexto —la próxima lección— el valor que pases satisfará este tipo, y un satisfies CarritoValor sobre el objeto te avisará si la forma se desvía. Si empaquetas el store con el patrón factory del nivel anterior, puedes incluso derivar el tipo con ReturnType en vez de escribirlo a mano.