Al proyectar interfaces genéricas, librerías y funciones utilitarias, una responsabilidad central del arquitecto radica en dirigir con precisión los puntos de inferencia de tipos (Type Inference).
Antes de TypeScript 5.4, el sistema de tipos presentaba una seria debilidad: si un parámetro de tipo T se empleaba en más de una posición de argumento, el compilador catalogaba cada posición como un candidato válido para deducir T. En consecuencia, el tipo resultante se sintetizaba como la unión amplia de todos los argumentos pasados:
// Problema previo a TypeScript 5.4: síntesis accidental de uniones
function createColorPalette<T extends string>(
allowedColors: T[],
defaultColor: T // ¡Deseamos que defaultColor proceda estrictamente de allowedColors!
) { /* ... */ }
// Un programador comete un error tipográfico: 'emerald' no existe en allowedColors
createColorPalette(['red', 'blue'], 'emerald');
// EN LUGAR DE EMITIR UN ERROR: ¡TypeScript infiere silenciosamente T como ('red' | 'blue' | 'emerald')!
// El error tipográfico en defaultColor amplió la paleta permitida, invalidando la seguridad estática.Para bloquear una posición específica de participar en la resolución de T, la comunidad concibió durante años complejas artimañas basadas en tipos condicionales (como type NoInferLegacy<T> = [T][T extends any ? 0 : never]), las cuales ralentizaban tsc y producían mensajes de error incomprensibles en los editores.
A partir de TypeScript 5.4+, se incorporó de forma nativa la utilidad NoInfer<T>.
La utilidad NoInfer<T> bloquea la inferencia en argumentos secundarios (valores predeterminados, callbacks, selectores): el compilador deduce el parámetro T únicamente a partir de fuentes primarias y limita las posiciones con NoInfer<T> a una estricta comprobación de asignabilidad.
La utilidad NoInfer<T> segrega las posiciones de argumentos de una función genérica en dos categorías nítidas:
T (por ejemplo, el catálogo de estados válidos de una máquina, los endpoints registrados o el esquema de columnas de una tabla).T ya computado (estados iniciales, valores por defecto, handlers de transición), pero desprovistos de capacidad para incorporar nuevos literales a T.Envolver un argumento en NoInfer<T> instruye al compilador: «Calcula el tipo T a partir de los restantes argumentos primarios y limítate a verificar si este parámetro específico es compatible con el tipo T ya consolidado».
+-------------------------------------------------------------------------+
| Mecánica de inferencia con NoInfer<T> |
+-------------------------------------------------------------------------+
| Sin NoInfer<T>: |
| fn<T>(candidates: T[], fallback: T) |
| 1. candidates ('red' | 'blue') ──> CANDIDATO PARA T |
| 2. fallback ('emerald') ──> CANDIDATO PARA T |
| RESULTADO T: ('red' | 'blue' | 'emerald') (¡Fallo silencioso!) |
+-------------------------------------------------------------------------+
│
▼ Solución en TypeScript 5.4+
+-------------------------------------------------------------------------+
| Con NoInfer<T>: |
| fn<T>(candidates: T[], fallback: NoInfer<T>) |
| 1. candidates ('red' | 'blue') ──> FUENTE EXCLUSIVA PARA T |
| RESULTADO T: ('red' | 'blue') |
| 2. fallback se somete a verificación de asignabilidad: |
| 'emerald' no pertenece a ('red' | 'blue') => ¡ERROR DE COMPILACIÓN! |
+-------------------------------------------------------------------------+El compilador evalúa las posiciones: todos los argumentos SIN NoInfer aportan sus tipos a la resolución de T.
Las posiciones envueltas en NoInfer<T> quedan temporalmente excluidas de participar en la síntesis de T.
El compilador consolida y congela el tipo concreto T basándose exclusivamente en las fuentes primarias.
El argumento en la posición NoInfer<T> se valida contra T; cualquier discrepancia arroja un error estático.
Una función se declara: 'function createSelect<T extends string>(options: T[], selected: NoInfer<T>): void'. ¿Qué sucede al invocar createSelect(['active', 'pending'], 'draft')?
En motores de estado: createMachine({ states, initialState }). El estado inicial initialState: NoInfer<TStates> debe ser necesariamente una de las claves de states. Un error tipográfico en initialState detiene la compilación en lugar de introducir estados fantasma.
En componentes visuales: defineButton({ variants: ['primary', 'outline', 'ghost'], defaultVariant: 'primary' }). El valor defaultVariant: NoInfer<TVariant> queda estrictamente vinculado al array de variantes definido.
En buses de eventos: bus.on(event: TEvent, handler: (payload: EventPayload<NoInfer<TEvent>>) => void). Evita bucles de retroalimentación donde la carga útil del callback modifique el tipo de evento registrado.
En proyecciones de datos: query.select(columns: TCol[], orderBy: NoInfer<TCol>). El parámetro orderBy se valida estrictamente contra las columnas seleccionadas, impidiendo ordenar por campos excluidos de la consulta.
Analizaremos la implementación de una máquina de estados donde el estado de arranque y los métodos de transición están blindados con NoInfer<TState>.
El desarrollador define el grafo en states. El genérico TState se deriva únicamente de estas propiedades.
El campo initialState se tipa como NoInfer<TState>, impidiéndole ampliar la unión de TState.
Suministrar un estado inválido produce un error de asignabilidad antes de la fase de compilación.
El método transitionTo(next: NoInfer<TState>) solo admite transiciones a estados válidos del grafo.
export interface StateConfig<TContext> {
readonly onEnter?: (context: TContext) => void;
readonly onExit?: (context: TContext) => void;
}
export interface MachineDefinition<TState extends string, TContext> {
readonly states: Record<TState, StateConfig<TContext>>;
// NoInfer<TState> previene que un error tipográfico en initialState
// contamine TState con un estado inexistente
readonly initialState: NoInfer<TState>;
readonly context: TContext;
}
export class StateMachine<TState extends string, TContext> {
private currentState: TState;
private readonly context: TContext;
private readonly states: Record<TState, StateConfig<TContext>>;
constructor(definition: MachineDefinition<TState, TContext>) {
this.currentState = definition.initialState;
this.context = definition.context;
this.states = definition.states;
}
public getState(): TState {
return this.currentState;
}
/**
* Transiciona al siguiente estado del grafo.
* ¡El argumento nextState también está protegido con NoInfer<TState>!
*/
public transitionTo(nextState: NoInfer<TState>): void {
if (!(nextState in this.states)) {
throw new Error(`Transición inválida hacia el estado: ${nextState}`);
}
this.states[this.currentState]?.onExit?.(this.context);
this.currentState = nextState;
this.states[this.currentState]?.onEnter?.(this.context);
}
}
/**
* Función factoría para crear la máquina de estados
*/
export function createStateMachine<TState extends string, TContext>(
definition: MachineDefinition<TState, TContext>
): StateMachine<TState, TContext> {
return new StateMachine(definition);
}¿Qué ocurre si un desarrollador declara 'function compare<T>(a: NoInfer<T>, b: NoInfer<T>): boolean' y llama compare('foo', 'bar') sin genéricos explícitos?
Máquina de estados de pasarela de pagos que emplea NoInfer<T> para neutralizar errores tipográficos en el estado inicial y prevenir transiciones hacia estados inválidos.
import { createStateMachine } from '@/lib/state-machine';
interface PaymentContext {
orderId: string;
amount: number;
}
// 1. Escenario exitoso: initialState ('PENDING') pertenece a states
export const paymentMachine = createStateMachine({
context: { orderId: 'ord_123', amount: 4500 },
states: {
PENDING: { onEnter: (ctx) => console.log('Aguardando cobro...', ctx.orderId) },
PROCESSING: {},
COMPLETED: {},
FAILED: {},
},
initialState: 'PENDING', // ✅ Válido
});
// 2. Errores estáticos de compilación:
export const brokenMachine = createStateMachine({
context: { orderId: 'ord_999', amount: 100 },
states: {
IDLE: {},
RUNNING: {},
},
// @ts-expect-error Error: 'STOPPED' no existe en el objeto states
initialState: 'STOPPED',
// Type '"STOPPED"' is not assignable to type '"IDLE" | "RUNNING"'.
});
// 3. Protección del método transitionTo:
paymentMachine.transitionTo('PROCESSING'); // ✅ Válido
// @ts-expect-error Error: Transición a estado no registrado bloqueada estáticamente
paymentMachine.transitionTo('REFUNDED');
// Argument of type '"REFUNDED"' is not assignable to parameter of type
// '"PENDING" | "PROCESSING" | "COMPLETED" | "FAILED"'.Si se envuelve el parámetro T en NoInfer<T> en todos los argumentos de la función: function fn<T>(a: NoInfer<T>, b: NoInfer<T>), el compilador carecerá de candidatos de inferencia. El tipo T no podrá deducirse y retrocederá a unknown. Al menos una posición debe actuar como fuente de deducción.
// ❌ ANTIPATRÓN: Sin fuentes de inferencia, T degenera a unknown
function compare<T>(a: NoInfer<T>, b: NoInfer<T>): boolean { return a === b; }
compare('foo', 'bar'); // T se resuelve como unknown, ¡validación anulada!
// ✅ CORRECTO: El primer argumento define el tipo, el segundo solo se valida
function compare<T>(primary: T, candidate: NoInfer<T>): boolean { return primary === candidate; }
// @ts-expect-error Error: 'bar' no es asignable a 'foo'
compare('foo', 'bar');La utilidad NoInfer<T> no suspende la verificación de asignabilidad; únicamente excluye al argumento del conjunto de candidatos que determinan el tipo T. El valor enviado sigue estando estrictamente obligado a cumplir el contrato del tipo consolidado.
Declarar function get<T>(): NoInfer<T> carece de propósito: el tipo de retorno de una función nunca actúa como candidato de inferencia para los parámetros. NoInfer está diseñado exclusivamente para argumentos de entrada.
El tipo utilitario NoInfer<T> en TypeScript 5.4+ es un recurso clave para diseñadores de APIs, DSLs y bibliotecas. Concede gobierno absoluto sobre qué argumentos determinan el tipo genérico y cuáles actúan únicamente como consumidores. Esto salvaguarda el código contra ampliaciones imprevistas de uniones y convierte errores tipográficos en alertas inmediatas de compilación.