NoInfer: controlando inferência em generics complexos
1. Entendendo o Problema: Quando a Inferência Automática Falha
O TypeScript possui um poderoso sistema de inferência de tipos, mas em cenários com generics complexos, essa inferência pode se tornar excessiva e produzir resultados indesejados. Um exemplo clássico ocorre quando usamos Array.filter com uniões de tipos:
type Status = "active" | "inactive" | "pending";
const statuses: Status[] = ["active", "inactive", "pending"];
// Problema: TypeScript infere o tipo mais amplo
const filtered = statuses.filter((s) => s !== "pending");
// Tipo inferido: Status[] (perdeu a informação de que "pending" foi excluído)
Outro cenário comum é em funções de validação de formulários, onde o objeto de validação pode poluir o tipo inferido:
function createValidator<T>(schema: Record<keyof T, (value: any) => boolean>) {
return schema;
}
interface User {
name: string;
age: number;
}
// Inferência excessiva: o tipo T se torna muito amplo
const validator = createValidator({
name: (v) => typeof v === "string",
age: (v) => typeof v === "number",
extraField: (v) => true, // Isso expande T indevidamente
});
2. O que é o Utilitário NoInfer<T>?
Introduzido no TypeScript 5.4, NoInfer<T> é um tipo utilitário que "esconde" um tipo da inferência do compilador. Diferente de Exclude (que remove tipos) ou Extract (que extrai tipos), NoInfer apenas torna o tipo invisível para o algoritmo de inferência, sem alterar sua estrutura.
Definição oficial simplificada:
type NoInfer<T> = [T][T extends any ? 0 : never];
Isso cria um tipo que o compilador não usa para deduzir o tipo genérico, mas que ainda é verificado em tempo de compilação.
3. Sintaxe e Uso Básico do NoInfer
Vamos ver um exemplo fundamental que demonstra o poder do NoInfer:
// Sem NoInfer - inferência problemática
function createConfig<T>(defaultValue: T, options: T[]) {
return { defaultValue, options };
}
const config1 = createConfig("hello", ["world", 42]);
// T inferido como string | number (muito amplo)
// Com NoInfer - controle da inferência
function createConfigFixed<T>(
defaultValue: T,
options: NoInfer<T>[]
) {
return { defaultValue, options };
}
const config2 = createConfigFixed("hello", ["world", "typescript"]);
// T corretamente inferido como string
// Erro se tentar: createConfigFixed("hello", ["world", 42]);
A diferença é clara: com NoInfer, apenas o defaultValue guia a inferência, enquanto as options são verificadas para serem compatíveis, mas não influenciam o tipo genérico.
4. Casos de Uso Práticos em Generics Complexos
Funções de validação com lookup
interface ValidationRules {
required?: boolean;
minLength?: number;
maxLength?: number;
}
function createFieldValidator<T extends Record<string, any>>(
fields: T,
rules: { [K in keyof T]?: NoInfer<ValidationRules> }
) {
return { fields, rules };
}
const userValidator = createFieldValidator(
{ name: "John", email: "john@example.com" },
{ name: { required: true, minLength: 2 } }
// rules não expande o tipo T
);
Reducers com estado inicial
type Action =
| { type: "ADD"; payload: string }
| { type: "REMOVE"; payload: number }
| { type: "CLEAR" };
function createReducer<TState>(
initialState: TState,
reducer: (state: TState, action: NoInfer<Action>) => TState
) {
return { initialState, reducer };
}
interface AppState {
items: string[];
count: number;
}
const reducer = createReducer(
{ items: [], count: 0 },
(state, action) => {
switch (action.type) {
case "ADD":
return { ...state, items: [...state.items, action.payload] };
case "REMOVE":
return { ...state, items: state.items.filter((_, i) => i !== action.payload) };
case "CLEAR":
return { ...state, items: [] };
}
}
);
APIs de configuração
type LogLevel = "debug" | "info" | "warn" | "error";
interface LoggerConfig {
level: LogLevel;
prefix?: string;
transports?: string[];
}
function createLogger<T extends LoggerConfig>(config: T) {
return {
log: (message: string) => console.log(`[${config.level}] ${message}`),
config,
};
}
// Sem NoInfer, o tipo T pode ser expandido
const logger = createLogger({
level: "info",
prefix: "App",
extraField: true, // Isso expande T indevidamente
});
// Com NoInfer
function createLoggerFixed<T extends LoggerConfig>(
config: NoInfer<T> & LoggerConfig
) {
return {
log: (message: string) => console.log(`[${config.level}] ${message}`),
config,
};
}
5. Combinando NoInfer com Outros Tipos Utilitários
NoInfer + Partial
function createPartialConfig<T>(
base: T,
overrides: Partial<NoInfer<T>>
): T {
return { ...base, ...overrides };
}
const config = createPartialConfig(
{ host: "localhost", port: 3000, ssl: true },
{ port: 8080 } // Apenas propriedades de T são permitidas
);
NoInfer + keyof
function getProperty<T, K extends keyof T>(
obj: T,
key: K
): T[K] {
return obj[key];
}
function getPropertySafe<T, K extends keyof NoInfer<T>>(
obj: T,
key: K
): T[K] {
return obj[key];
}
const data = { name: "John", age: 30 };
getPropertySafe(data, "name"); // OK
NoInfer + Conditional Types
type IsString<T> = T extends string ? "yes" : "no";
function processValue<T>(
value: T,
handler: (v: NoInfer<T>) => IsString<NoInfer<T>>
) {
return handler(value);
}
const result = processValue("hello", (v) => "yes"); // Tipo: "yes"
6. Limitações e Boas Práticas
Onde NoInfer não funciona:
// Não funciona bem com inferência contextual em callbacks
function createHandler<T>(callback: (value: NoInfer<T>) => void) {
return callback;
}
const handler = createHandler((value) => {
// value: unknown - inferência contextual falha
});
Boas práticas:
- Use
NoInferapenas quando necessário para controlar inferência excessiva - Prefira
as constpara literais quando possível - Considere
satisfiespara validação sem inferência - Evite usar
NoInferem parâmetros que dependem de inferência contextual
7. Exemplo Completo: Tipando uma API de Filtros com NoInfer
type FilterOperator = "equals" | "contains" | "greaterThan" | "lessThan";
interface FilterConfig<T> {
field: keyof T;
operator: FilterOperator;
value: any;
}
function createFilter<T extends Record<string, any>>(
schema: T,
filters: NoInfer<FilterConfig<T>>[]
) {
return {
schema,
filters,
apply: (data: T[]) => {
return data.filter((item) => {
return filters.every((filter) => {
const value = item[filter.field];
switch (filter.operator) {
case "equals":
return value === filter.value;
case "contains":
return String(value).includes(String(filter.value));
case "greaterThan":
return Number(value) > Number(filter.value);
case "lessThan":
return Number(value) < Number(filter.value);
default:
return true;
}
});
});
},
};
}
// Uso correto
interface Product {
id: number;
name: string;
price: number;
category: string;
}
const productFilter = createFilter(
{ id: 1, name: "Product", price: 100, category: "Electronics" },
[
{ field: "price", operator: "greaterThan", value: 50 },
{ field: "category", operator: "equals", value: "Electronics" },
]
);
// Teste de tipos
const products: Product[] = [
{ id: 1, name: "Laptop", price: 1200, category: "Electronics" },
{ id: 2, name: "Mouse", price: 30, category: "Electronics" },
];
const filtered = productFilter.apply(products);
// filtered: Product[] - tipo preservado corretamente
// Erro de compilação se tentar campo inválido
// productFilter.apply([
// { id: 3, name: "Book", price: 20, category: "Books", invalidField: true }
// ]);
Referências
- Documentação oficial do TypeScript - NoInfer — Documentação oficial do tipo utilitário NoInfer
- TypeScript 5.4 Release Notes - NoInfer — Anúncio oficial do TypeScript 5.4 com detalhes sobre NoInfer
- TypeScript Deep Dive - Utility Types — Guia abrangente sobre tipos utilitários do TypeScript
- Total TypeScript - NoInfer Guide — Tutorial prático sobre o uso de NoInfer em cenários reais
- TypeScript Playground - NoInfer Examples — Exemplos interativos de NoInfer no TypeScript Playground
- TypeScript Handbook - Generics — Guia completo sobre generics no TypeScript, base para entender NoInfer