Errores típicos y tipado
Los fallos que todo el mundo comete con signals: copiar o desestructurar el valor y perder la reactividad, olvidar los paréntesis del getter, pasar el valor en vez de la función. Y cómo tiparlos bien en TypeScript: inferencia, el caso undefined de createSignal sin argumento, y los tipos Accessor y Setter en props.
Los signals son fáciles de usar y fáciles de romper de maneras que no dan error. Casi todos los bugs de “no se actualiza” en Solid tienen la misma raíz: en algún punto se copió el valor y se cortó el cable que lo unía a su fuente. Cerramos el nivel del átomo catalogando esos errores para reconocerlos al vuelo, y añadiendo la capa que los previene de raíz: un buen tipado en TypeScript.
- Reconocer los cuatro patrones que rompen la reactividad de un signal.
- Entender por qué desestructurar o copiar el valor pierde el rastreo.
- Tipar signals con inferencia y con parámetros de tipo explícitos.
- Manejar el caso
undefinedde un signal sin valor inicial y tiparAccessorySetter.
Los cuatro clásicos que rompen la reactividad
Todos los fallos de reactividad con signals son variaciones de un mismo pecado —leer demasiado pronto y guardar el resultado— pero conviene tenerlos catalogados:
Copiar el valor
const v = count() en la cima del componente congela una foto. El componente corre una vez, así que v no vuelve a cambiar. Usa count() en el punto de uso.
Desestructurar props
const { valor } = props lee la prop una vez y la desconecta. Las props de Solid son reactivas: hay que acceder a props.valor en el punto de uso.
Pasar el valor, no la función
Entregar count() a un hijo o a un helper le da un número muerto. Pasa count para que el consumidor se suscriba por su cuenta.
Leer fuera de rastreo
Llamar a count() en un setTimeout o en una función suelta y esperar que “reaccione”. Fuera de un ámbito de rastreo no hay suscripción.
El caso de las props merece una nota: en Solid, props es un objeto reactivo (un proxy), y desestructurarlo con const { valor } = props evalúa valor una sola vez, perdiendo la reactividad igual que copiar un signal. Es el error más frecuente al llegar desde React, y su solución —acceder siempre como props.valor, o separar con splitProps y fijar defaults con mergeProps— es tema de un nivel propio. Lo mencionamos aquí porque comparte exactamente la misma raíz que copiar un getter.
// MAL: desconecta las props reactivas
function Saludo(props: { nombre: string }) {
const { nombre } = props; // foto
return <p>Hola, {nombre}</p>; // no reacciona
}
// BIEN: accede en el punto de uso
function Saludo(props: { nombre: string }) {
return <p>Hola, {props.nombre}</p>;
}
Tipar signals en TypeScript
Con un valor inicial, TypeScript infiere el tipo y rara vez necesitas anotar nada: createSignal(0) produce un Signal<number>, createSignal("") un Signal<string>. La inferencia es tu primera línea de defensa, porque convierte “pasé el tipo equivocado” en un error de compilación.
const [count, setCount] = createSignal(0); // Signal<number>
setCount("x"); // Error de tipos: string no es asignable a number
Cuando el valor inicial no basta para inferir el tipo que quieres —un array vacío, una unión, un objeto que admite null— pasa el parámetro de tipo explícito:
type Todo = { id: number; texto: string; hecho: boolean };
const [todos, setTodos] = createSignal<Todo[]>([]); // no es never[]
const [user, setUser] = createSignal<User | null>(null); // admite ambos
Si llamas a createSignal sin argumento, el valor arranca como undefined, y TypeScript lo refleja: createSignal<string>() no te da un Accessor<string>, sino un Accessor<string | undefined>. Esto es correcto y honesto —el signal puede estar vacío— pero te obliga a estrechar el tipo al leerlo. Si de verdad quieres un tipo no opcional, o das un valor inicial, o asumes la comprobación de undefined en cada lectura. Ignorarlo produce el clásico error de “puede ser undefined” justo donde no lo esperabas.
Para tipar un signal que recibes —en las props de un componente o como argumento de una función— usa los tipos exportados por Solid. El getter es un Accessor<T> y el setter un Setter<T>; la tupla completa es un Signal<T>:
import type { Accessor, Setter } from "solid-js";
function Panel(props: { count: Accessor<number>; setCount: Setter<number> }) {
return (
<button onClick={() => props.setCount(c => c + 1)}>
{props.count()}
</button>
);
}
Tipar la prop como Accessor<number> y no como number documenta en la firma que esperas una lectura reactiva, no una foto, y hace que pasar count() por error sea un fallo de compilación en vez de un bug silencioso. El sistema de tipos se convierte así en el guardián de la regla “pasa la función, no el valor”.
Si destilas todos los errores de este nivel hasta su esencia, queda una sola frase: la reactividad se rompe cuando extraes el valor de su fuente antes de tiempo. Copiar count() en una variable, desestructurar props, pasar el número en lugar del getter, leer en un contexto que no rastrea: son cuatro disfraces del mismo acto de arrancar el dato de la función que sabía cómo mantenerlo vivo. Y la razón por la que este error es tan universal no es descuido, es hábito: venimos de un mundo donde un valor es un valor y copiarlo es inofensivo, donde const x = obj.y es la operación más trivial que existe. En Solid esa trivialidad es precisamente la trampa, porque aquí el valor no es solo un dato: es el extremo visible de una suscripción, y copiarlo se lleva el dato pero deja atrás la suscripción. La disciplina que cura todos estos bugs de golpe es una sola y se enuncia fácil: retrasa la lectura hasta el último momento posible, el punto exacto donde el DOM consume el dato, y hasta entonces mueve funciones, no resultados. TypeScript, bien usado, convierte esa disciplina en una barandilla: tipa lo que recibes como Accessor<T> en vez de T y el compilador te impedirá cometer la copia prematura, porque una función y un número dejan de ser intercambiables ante sus ojos. La maestría con signals no consiste en aprender más API —ya la conoces entera: un getter, un setter, una opción— sino en desaprender el reflejo de copiar. El día que tu instinto por defecto sea pasar count y no count(), dejar de leer en la cima y empezar a leer en la hoja del árbol donde el dato se pinta, ese día los bugs de “no se actualiza” desaparecen de tu código, no porque los caces, sino porque ya no los escribes.
- Reproduce los cuatro clásicos uno a uno —copiar el valor, desestructurar props, pasar el valor, leer fuera de rastreo— y arregla cada uno.
- Declara
createSignal<Todo[]>([])y comprueba que, sin el parámetro de tipo, el array vacío se infiere como un tipo inservible. - Crea
createSignal<string>()sin argumento e inspecciona en el editor por qué el tipo del getter incluyeundefined. - Tipa un componente que reciba
count: Accessor<number>e intenta pasarlecount()en vez decount; confirma que TypeScript lo rechaza. - Escribe con tus palabras la regla única que, bien aplicada, previene los cuatro errores a la vez.