---
description: How to use TypeScript's NoInfer utility to control type inference
  and keep your generics predictable.
keywords:
  - TypeScript
  - NoInfer
  - Type Inference
  - Generics
  - TypeScript 5.4
publishDate: 2025-11-06
tags:
  - TypeScript
title: Prevent TypeScript from Inferring
---

Type inference is usually a win.

It removes boilerplate. It keeps APIs ergonomic. It makes generics feel invisible.

It also has a failure mode.

Sometimes TypeScript infers from a place you did not intend.

This post shows a common way that happens with generics and callbacks, then fixes it with `NoInfer`.

> [!NOTE]
> I use a form component as a concrete example. The pattern shows up anywhere you have generics plus callbacks.

## Step 0: `any` breaks the type link

Start with a simple form component.

It accepts `initialValues` and an `onSubmit` callback.

```tsx twoslash
// @jsx: react-jsx
interface FormProps {
  initialValues: any;
  onSubmit?: (values: any) => void;
}

function Form({ initialValues, onSubmit }: FormProps) {
  return <form />;
}
```

This compiles, but it gives you nothing.

The `values` parameter is `any`, so TypeScript cannot protect you.

```tsx twoslash
// @jsx: react-jsx
interface FormProps {
  initialValues: any;
  onSubmit?: (values: any) => void;
}

function Form({ initialValues, onSubmit }: FormProps) {
  return <form />;
}
// ---cut---
function App() {
  return (
    <Form
      initialValues={{ name: "", email: "" }}
      onSubmit={(values) => {
        //        ^?
        console.log(values.name, values.email);
      }}
    />
  );
}
```

If the form shape changes or you mistype a property, TypeScript stays quiet.

## Step 1: make the component generic

The obvious fix is to make `Form` generic and let `initialValues` drive the type.

```tsx twoslash
// @jsx: react-jsx
interface FormProps<T> {
  initialValues: T;
  onSubmit?: (values: T) => void;
}

function Form<T>({ initialValues, onSubmit }: FormProps<T>) {
  return <form />;
}
```

Now `initialValues` becomes the source of truth.

TypeScript infers `T` from what you pass in.

```tsx twoslash
// @jsx: react-jsx
interface FormProps<T> {
  initialValues: T;
  onSubmit?: (values: T) => void;
}

function Form<T>({ initialValues, onSubmit }: FormProps<T>) {
  return <form />;
}
// ---cut---
function App() {
  return (
    <Form
      initialValues={{ name: "", email: "" }}
      onSubmit={(values) => {
//               ^?
// @noErrors
        console.log(values.)
//                         ^|
      }}
    />
  );
}
```

You get autocomplete and type safety.

And if you access a property that does not exist, TypeScript catches it.

```tsx twoslash
// @errors: 2339
// @jsx: react-jsx
interface FormProps<T> {
  initialValues: T;
  onSubmit?: (values: T) => void;
}

function Form<T>({ initialValues, onSubmit }: FormProps<T>) {
  return <form />;
}
// ---cut---
function App() {
  return (
    <Form
      initialValues={{ name: "", email: "" }}
      onSubmit={(values) => {
        console.log(values.address);
      }}
    />
  );
}
```

So far this looks perfect.

Then refactors happen.

## Step 2: inference can come from the wrong place

Here is the scenario that bites.

Before generics, people often "fixed" the callback by annotating it manually.

That was the only way to get types.

```tsx twoslash
// @jsx: react-jsx
interface FormProps {
  initialValues: any;
  onSubmit?: (values: any) => void;
}

function Form({ initialValues, onSubmit }: FormProps) {
  return <form />;
}
// ---cut---
const initialValues: any = {};

function App() {
  return (
    <Form
      initialValues={initialValues}
      onSubmit={(values: { name: string; email: string }) => {
        console.log(values.name, values.email);
      }}
    />
  );
}
```

Then you add generics.

```tsx twoslash
// @jsx: react-jsx
interface FormProps<T> {
  initialValues: T;
  onSubmit?: (values: T) => void;
}

function Form<T>({ initialValues, onSubmit }: FormProps<T>) {
  return <form />;
}

const initialValues: any = {};
// ---cut---
function App() {
  return (
    <Form
      //^?
      initialValues={initialValues}
      onSubmit={(values: { name: string; email: string }) => {
        console.log(values.name, values.email);
      }}
    />
  );
}
```

Here is what just happened.

Because `initialValues` is `any`, it provides no useful signal for inference.

So TypeScript looks elsewhere.

It sees the annotated callback and infers `T` from that instead.

That means your callback annotation becomes the source of truth.

Not `initialValues`.

That is backwards.

This is the bug.

You wanted inference to come from the values.

TypeScript inferred from the callback.

## Step 3: tell TypeScript where not to infer

This is what `NoInfer` is for.

Wrap the callback parameter in `NoInfer<T>`.

```tsx twoslash
// @jsx: react-jsx
interface FormProps<T> {
  initialValues: T;
  onSubmit?: (values: NoInfer<T>) => void;
}

function Form<T>({ initialValues, onSubmit }: FormProps<T>) {
  return <form />;
}
```

Read that as:

"`values` is `T`, but do not use this position to infer `T`."

Now inference must come from `initialValues`.

```tsx twoslash
// @jsx: react-jsx
interface FormProps<T> {
  initialValues: T;
  onSubmit?: (values: NoInfer<T>) => void;
}

function Form<T>({ initialValues, onSubmit }: FormProps<T>) {
  return <form />;
}
// ---cut---
function App() {
  return (
    <Form
      initialValues={{ name: "", email: "" }}
      onSubmit={(values) => {
        //        ^?
        console.log(values.name, values.email);
      }}
    />
  );
}
```

And if someone keeps an old annotation that does not match, TypeScript stops them.

```tsx twoslash
// @errors: 2322
// @jsx: react-jsx
interface FormProps<T> {
  initialValues: T;
  onSubmit?: (values: NoInfer<T>) => void;
}

function Form<T>({ initialValues, onSubmit }: FormProps<T>) {
  return <form />;
}
// ---cut---
function App() {
  return (
    <Form
      initialValues={{ name: "", email: "" }}
      onSubmit={(values: { name: string; email: string; address: string }) => {
        console.log(values.name);
      }}
    />
  );
}
```

That is exactly what you want during a refactor.

Old annotations should not silently redefine your generic.

## If you are not on TypeScript 5.4

`NoInfer` shipped in TypeScript 5.4.

If you are stuck on an older version, you can approximate the behavior with a type trick:

```ts
type NoInfer<T> = [T][T extends any ? 0 : never];
```

It creates an inference barrier while preserving the underlying structure of `T`.

Once you upgrade, prefer the built-in `NoInfer`.

## Behind the scenes

In lib.d.ts, `NoInfer` is defined like this:

```ts
type NoInfer<T> = intrinsic;
```

That "intrinsic" part matters.

It means the compiler implements the behavior directly.

TypeScript treats `NoInfer<T>` as "this is `T`, but do not collect inference candidates from here."

So the type stays the same, but inference ignores that position.

## Recap

Inference is not magic. It is a set of rules.

When a generic appears in multiple positions, TypeScript can infer from any of them.

Sometimes that is helpful.

Sometimes it picks the wrong source.

Use `NoInfer<T>` when:

- You want inference to come from one argument
- You want callbacks to follow that inferred type
- You do not want parameter annotations to redefine your generic during refactors

This keeps your generics predictable.

And it keeps the "source of truth" where you intended it to be.