Back to blog

Prevent TypeScript from Inferring

3 min read

Cover image for Prevent TypeScript from Inferring
Image crafted by robots

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.

Start with a simple form component.

It accepts initialValues and an onSubmit callback.

interface
interface FormProps
FormProps
{
FormProps.initialValues: any
initialValues
: any;
FormProps.onSubmit?: (values: any) => void
onSubmit
?: (
values: any
values
: any) => void;
}
function
function Form({ initialValues, onSubmit }: FormProps): JSX.Element
Form
({
initialValues: any
initialValues
,
onSubmit: ((values: any) => void) | undefined
onSubmit
}:
interface FormProps
FormProps
) {
return <
React.JSX.IntrinsicElements.form: React.DetailedHTMLProps<React.FormHTMLAttributes<HTMLFormElement>, HTMLFormElement>
form
/>;
}

This compiles, but it gives you nothing.

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

function
function App(): JSX.Element
App
() {
return (
<
function Form({ initialValues, onSubmit }: FormProps): JSX.Element
Form
FormProps.initialValues: any
initialValues
={{
name: string
name
: "",
email: string
email
: "" }}
FormProps.onSubmit?: (values: any) => void
onSubmit
={(values) => {
values: any
var console: Console

The console module provides a simple debugging console that is similar to the JavaScript console mechanism provided by web browsers.

The module exports two specific components:

  • A Console class with methods such as console.log(), console.error() and console.warn() that can be used to write to any Node.js stream.
  • A global console instance configured to write to process.stdout and process.stderr. The global console can be used without importing the node:console module.

Warning: The global console object's methods are neither consistently synchronous like the browser APIs they resemble, nor are they consistently asynchronous like all other Node.js streams. See the note on process I/O for more information.

Example using the global console:

console.log('hello world');
// Prints: hello world, to stdout
console.log('hello %s', 'world');
// Prints: hello world, to stdout
console.error(new Error('Whoops, something bad happened'));
// Prints error message and stack trace to stderr:
// Error: Whoops, something bad happened
// at [eval]:5:15
// at Script.runInThisContext (node:vm:132:18)
// at Object.runInThisContext (node:vm:309:38)
// at node:internal/process/execution:77:19
// at [eval]-wrapper:6:22
// at evalScript (node:internal/process/execution:76:60)
// at node:internal/main/eval_string:23:3
const name = 'Will Robinson';
console.warn(`Danger ${name}! Danger!`);
// Prints: Danger Will Robinson! Danger!, to stderr

Example using the Console class:

const out = getStreamSomehow();
const err = getStreamSomehow();
const myConsole = new console.Console(out, err);
myConsole.log('hello world');
// Prints: hello world, to out
myConsole.log('hello %s', 'world');
// Prints: hello world, to out
myConsole.error(new Error('Whoops, something bad happened'));
// Prints: [Error: Whoops, something bad happened], to err
const name = 'Will Robinson';
myConsole.warn(`Danger ${name}! Danger!`);
// Prints: Danger Will Robinson! Danger!, to err

@seesource

console
.
Console.log(message?: any, ...optionalParams: any[]): void (+1 overload)

Prints to stdout with newline. Multiple arguments can be passed, with the first used as the primary message and all additional used as substitution values similar to printf(3) (the arguments are all passed to util.format()).

const count = 5;
console.log('count: %d', count);
// Prints: count: 5, to stdout
console.log('count:', count);
// Prints: count: 5, to stdout

See util.format() for more information.

@sincev0.1.100

log
(
values: any
values
.
any
name
,
values: any
values
.
any
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.

interface
interface FormProps<T>
FormProps
<
function (type parameter) T in FormProps<T>
T
> {
FormProps<T>.initialValues: T
initialValues
:
function (type parameter) T in FormProps<T>
T
;
FormProps<T>.onSubmit?: (values: T) => void
onSubmit
?: (
values: T
values
:
function (type parameter) T in FormProps<T>
T
) => void;
}
function
function Form<T>({ initialValues, onSubmit }: FormProps<T>): JSX.Element
Form
<
function (type parameter) T in Form<T>({ initialValues, onSubmit }: FormProps<T>): JSX.Element
T
>({
initialValues: T
initialValues
,
onSubmit: ((values: T) => void) | undefined
onSubmit
}:
interface FormProps<T>
FormProps
<
function (type parameter) T in Form<T>({ initialValues, onSubmit }: FormProps<T>): JSX.Element
T
>) {
return <
React.JSX.IntrinsicElements.form: React.DetailedHTMLProps<React.FormHTMLAttributes<HTMLFormElement>, HTMLFormElement>
form
/>;
}

Now initialValues becomes the source of truth.

TypeScript infers T from what you pass in.

function
function App(): JSX.Element
App
() {
return (
<
function Form<{
name: string;
email: string;
}>({ initialValues, onSubmit }: FormProps<{
name: string;
email: string;
}>): JSX.Element
Form
FormProps<{ name: string; email: string; }>.initialValues: {
name: string;
email: string;
}
initialValues
={{
name: string
name
: "",
email: string
email
: "" }}
FormProps<{ name: string; email: string; }>.onSubmit?: (values: {
name: string;
email: string;
}) => void
onSubmit
={(values) => {
values: {
name: string;
email: string;
}
var console: Console

The console module provides a simple debugging console that is similar to the JavaScript console mechanism provided by web browsers.

The module exports two specific components:

  • A Console class with methods such as console.log(), console.error() and console.warn() that can be used to write to any Node.js stream.
  • A global console instance configured to write to process.stdout and process.stderr. The global console can be used without importing the node:console module.

Warning: The global console object's methods are neither consistently synchronous like the browser APIs they resemble, nor are they consistently asynchronous like all other Node.js streams. See the note on process I/O for more information.

Example using the global console:

console.log('hello world');
// Prints: hello world, to stdout
console.log('hello %s', 'world');
// Prints: hello world, to stdout
console.error(new Error('Whoops, something bad happened'));
// Prints error message and stack trace to stderr:
// Error: Whoops, something bad happened
// at [eval]:5:15
// at Script.runInThisContext (node:vm:132:18)
// at Object.runInThisContext (node:vm:309:38)
// at node:internal/process/execution:77:19
// at [eval]-wrapper:6:22
// at evalScript (node:internal/process/execution:76:60)
// at node:internal/main/eval_string:23:3
const name = 'Will Robinson';
console.warn(`Danger ${name}! Danger!`);
// Prints: Danger Will Robinson! Danger!, to stderr

Example using the Console class:

const out = getStreamSomehow();
const err = getStreamSomehow();
const myConsole = new console.Console(out, err);
myConsole.log('hello world');
// Prints: hello world, to out
myConsole.log('hello %s', 'world');
// Prints: hello world, to out
myConsole.error(new Error('Whoops, something bad happened'));
// Prints: [Error: Whoops, something bad happened], to err
const name = 'Will Robinson';
myConsole.warn(`Danger ${name}! Danger!`);
// Prints: Danger Will Robinson! Danger!, to err

@seesource

console
.
Console.log(message?: any, ...optionalParams: any[]): void (+1 overload)

Prints to stdout with newline. Multiple arguments can be passed, with the first used as the primary message and all additional used as substitution values similar to printf(3) (the arguments are all passed to util.format()).

const count = 5;
console.log('count: %d', count);
// Prints: count: 5, to stdout
console.log('count:', count);
// Prints: count: 5, to stdout

See util.format() for more information.

@sincev0.1.100

log
(
values: {
name: string;
email: string;
}
values
. )
email
name
}}
/>
);
}

You get autocomplete and type safety.

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

function
function App(): JSX.Element
App
() {
return (
<
function Form<{
name: string;
email: string;
}>({ initialValues, onSubmit }: FormProps<{
name: string;
email: string;
}>): JSX.Element
Form
FormProps<{ name: string; email: string; }>.initialValues: {
name: string;
email: string;
}
initialValues
={{
name: string
name
: "",
email: string
email
: "" }}
FormProps<{ name: string; email: string; }>.onSubmit?: (values: {
name: string;
email: string;
}) => void
onSubmit
={(
values: {
name: string;
email: string;
}
values
) => {
var console: Console

The console module provides a simple debugging console that is similar to the JavaScript console mechanism provided by web browsers.

The module exports two specific components:

  • A Console class with methods such as console.log(), console.error() and console.warn() that can be used to write to any Node.js stream.
  • A global console instance configured to write to process.stdout and process.stderr. The global console can be used without importing the node:console module.

Warning: The global console object's methods are neither consistently synchronous like the browser APIs they resemble, nor are they consistently asynchronous like all other Node.js streams. See the note on process I/O for more information.

Example using the global console:

console.log('hello world');
// Prints: hello world, to stdout
console.log('hello %s', 'world');
// Prints: hello world, to stdout
console.error(new Error('Whoops, something bad happened'));
// Prints error message and stack trace to stderr:
// Error: Whoops, something bad happened
// at [eval]:5:15
// at Script.runInThisContext (node:vm:132:18)
// at Object.runInThisContext (node:vm:309:38)
// at node:internal/process/execution:77:19
// at [eval]-wrapper:6:22
// at evalScript (node:internal/process/execution:76:60)
// at node:internal/main/eval_string:23:3
const name = 'Will Robinson';
console.warn(`Danger ${name}! Danger!`);
// Prints: Danger Will Robinson! Danger!, to stderr

Example using the Console class:

const out = getStreamSomehow();
const err = getStreamSomehow();
const myConsole = new console.Console(out, err);
myConsole.log('hello world');
// Prints: hello world, to out
myConsole.log('hello %s', 'world');
// Prints: hello world, to out
myConsole.error(new Error('Whoops, something bad happened'));
// Prints: [Error: Whoops, something bad happened], to err
const name = 'Will Robinson';
myConsole.warn(`Danger ${name}! Danger!`);
// Prints: Danger Will Robinson! Danger!, to err

@seesource

console
.
Console.log(message?: any, ...optionalParams: any[]): void (+1 overload)

Prints to stdout with newline. Multiple arguments can be passed, with the first used as the primary message and all additional used as substitution values similar to printf(3) (the arguments are all passed to util.format()).

const count = 5;
console.log('count: %d', count);
// Prints: count: 5, to stdout
console.log('count:', count);
// Prints: count: 5, to stdout

See util.format() for more information.

@sincev0.1.100

log
(
values: {
name: string;
email: string;
}
values
.address);
Error ts(2339) ― Property 'address' does not exist on type '{ name: string; email: string; }'.
}}
/>
);
}

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.

const
const initialValues: any
initialValues
: any = {};
function
function App(): JSX.Element
App
() {
return (
<
function Form({ initialValues, onSubmit }: FormProps): JSX.Element
Form
FormProps.initialValues: any
initialValues
={
const initialValues: any
initialValues
}
FormProps.onSubmit?: (values: any) => void
onSubmit
={(
values: {
name: string;
email: string;
}
values
: {
name: string
name
: string;
email: string
email
: string }) => {
var console: Console

The console module provides a simple debugging console that is similar to the JavaScript console mechanism provided by web browsers.

The module exports two specific components:

  • A Console class with methods such as console.log(), console.error() and console.warn() that can be used to write to any Node.js stream.
  • A global console instance configured to write to process.stdout and process.stderr. The global console can be used without importing the node:console module.

Warning: The global console object's methods are neither consistently synchronous like the browser APIs they resemble, nor are they consistently asynchronous like all other Node.js streams. See the note on process I/O for more information.

Example using the global console:

console.log('hello world');
// Prints: hello world, to stdout
console.log('hello %s', 'world');
// Prints: hello world, to stdout
console.error(new Error('Whoops, something bad happened'));
// Prints error message and stack trace to stderr:
// Error: Whoops, something bad happened
// at [eval]:5:15
// at Script.runInThisContext (node:vm:132:18)
// at Object.runInThisContext (node:vm:309:38)
// at node:internal/process/execution:77:19
// at [eval]-wrapper:6:22
// at evalScript (node:internal/process/execution:76:60)
// at node:internal/main/eval_string:23:3
const name = 'Will Robinson';
console.warn(`Danger ${name}! Danger!`);
// Prints: Danger Will Robinson! Danger!, to stderr

Example using the Console class:

const out = getStreamSomehow();
const err = getStreamSomehow();
const myConsole = new console.Console(out, err);
myConsole.log('hello world');
// Prints: hello world, to out
myConsole.log('hello %s', 'world');
// Prints: hello world, to out
myConsole.error(new Error('Whoops, something bad happened'));
// Prints: [Error: Whoops, something bad happened], to err
const name = 'Will Robinson';
myConsole.warn(`Danger ${name}! Danger!`);
// Prints: Danger Will Robinson! Danger!, to err

@seesource

console
.
Console.log(message?: any, ...optionalParams: any[]): void (+1 overload)

Prints to stdout with newline. Multiple arguments can be passed, with the first used as the primary message and all additional used as substitution values similar to printf(3) (the arguments are all passed to util.format()).

const count = 5;
console.log('count: %d', count);
// Prints: count: 5, to stdout
console.log('count:', count);
// Prints: count: 5, to stdout

See util.format() for more information.

@sincev0.1.100

log
(
values: {
name: string;
email: string;
}
values
.
name: string
name
,
values: {
name: string;
email: string;
}
values
.
email: string
email
);
}}
/>
);
}

Then you add generics.

function
function App(): JSX.Element
App
() {
return (
<Form
function Form<{
name: string;
email: string;
}>({ initialValues, onSubmit }: FormProps<{
name: string;
email: string;
}>): JSX.Element
FormProps<{ name: string; email: string; }>.initialValues: {
name: string;
email: string;
}
initialValues
={
const initialValues: any
initialValues
}
FormProps<{ name: string; email: string; }>.onSubmit?: (values: {
name: string;
email: string;
}) => void
onSubmit
={(
values: {
name: string;
email: string;
}
values
: {
name: string
name
: string;
email: string
email
: string }) => {
var console: Console

The console module provides a simple debugging console that is similar to the JavaScript console mechanism provided by web browsers.

The module exports two specific components:

  • A Console class with methods such as console.log(), console.error() and console.warn() that can be used to write to any Node.js stream.
  • A global console instance configured to write to process.stdout and process.stderr. The global console can be used without importing the node:console module.

Warning: The global console object's methods are neither consistently synchronous like the browser APIs they resemble, nor are they consistently asynchronous like all other Node.js streams. See the note on process I/O for more information.

Example using the global console:

console.log('hello world');
// Prints: hello world, to stdout
console.log('hello %s', 'world');
// Prints: hello world, to stdout
console.error(new Error('Whoops, something bad happened'));
// Prints error message and stack trace to stderr:
// Error: Whoops, something bad happened
// at [eval]:5:15
// at Script.runInThisContext (node:vm:132:18)
// at Object.runInThisContext (node:vm:309:38)
// at node:internal/process/execution:77:19
// at [eval]-wrapper:6:22
// at evalScript (node:internal/process/execution:76:60)
// at node:internal/main/eval_string:23:3
const name = 'Will Robinson';
console.warn(`Danger ${name}! Danger!`);
// Prints: Danger Will Robinson! Danger!, to stderr

Example using the Console class:

const out = getStreamSomehow();
const err = getStreamSomehow();
const myConsole = new console.Console(out, err);
myConsole.log('hello world');
// Prints: hello world, to out
myConsole.log('hello %s', 'world');
// Prints: hello world, to out
myConsole.error(new Error('Whoops, something bad happened'));
// Prints: [Error: Whoops, something bad happened], to err
const name = 'Will Robinson';
myConsole.warn(`Danger ${name}! Danger!`);
// Prints: Danger Will Robinson! Danger!, to err

@seesource

console
.
Console.log(message?: any, ...optionalParams: any[]): void (+1 overload)

Prints to stdout with newline. Multiple arguments can be passed, with the first used as the primary message and all additional used as substitution values similar to printf(3) (the arguments are all passed to util.format()).

const count = 5;
console.log('count: %d', count);
// Prints: count: 5, to stdout
console.log('count:', count);
// Prints: count: 5, to stdout

See util.format() for more information.

@sincev0.1.100

log
(
values: {
name: string;
email: string;
}
values
.
name: string
name
,
values: {
name: string;
email: string;
}
values
.
email: string
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>.

interface
interface FormProps<T>
FormProps
<
function (type parameter) T in FormProps<T>
T
> {
FormProps<T>.initialValues: T
initialValues
:
function (type parameter) T in FormProps<T>
T
;
FormProps<T>.onSubmit?: (values: NoInfer<T>) => void
onSubmit
?: (
values: NoInfer<T>
values
:
type NoInfer<T> = intrinsic

Marker for non-inference type position

NoInfer
<
function (type parameter) T in FormProps<T>
T
>) => void;
}
function
function Form<T>({ initialValues, onSubmit }: FormProps<T>): JSX.Element
Form
<
function (type parameter) T in Form<T>({ initialValues, onSubmit }: FormProps<T>): JSX.Element
T
>({
initialValues: T
initialValues
,
onSubmit: ((values: NoInfer<T>) => void) | undefined
onSubmit
}:
interface FormProps<T>
FormProps
<
function (type parameter) T in Form<T>({ initialValues, onSubmit }: FormProps<T>): JSX.Element
T
>) {
return <
React.JSX.IntrinsicElements.form: React.DetailedHTMLProps<React.FormHTMLAttributes<HTMLFormElement>, HTMLFormElement>
form
/>;
}

Read that as:

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

Now inference must come from initialValues.

function
function App(): JSX.Element
App
() {
return (
<
function Form<{
name: string;
email: string;
}>({ initialValues, onSubmit }: FormProps<{
name: string;
email: string;
}>): JSX.Element
Form
FormProps<{ name: string; email: string; }>.initialValues: {
name: string;
email: string;
}
initialValues
={{
name: string
name
: "",
email: string
email
: "" }}
FormProps<{ name: string; email: string; }>.onSubmit?: (values: NoInfer<{
name: string;
email: string;
}>) => void
onSubmit
={(values) => {
values: NoInfer<{
name: string;
email: string;
}>
var console: Console

The console module provides a simple debugging console that is similar to the JavaScript console mechanism provided by web browsers.

The module exports two specific components:

  • A Console class with methods such as console.log(), console.error() and console.warn() that can be used to write to any Node.js stream.
  • A global console instance configured to write to process.stdout and process.stderr. The global console can be used without importing the node:console module.

Warning: The global console object's methods are neither consistently synchronous like the browser APIs they resemble, nor are they consistently asynchronous like all other Node.js streams. See the note on process I/O for more information.

Example using the global console:

console.log('hello world');
// Prints: hello world, to stdout
console.log('hello %s', 'world');
// Prints: hello world, to stdout
console.error(new Error('Whoops, something bad happened'));
// Prints error message and stack trace to stderr:
// Error: Whoops, something bad happened
// at [eval]:5:15
// at Script.runInThisContext (node:vm:132:18)
// at Object.runInThisContext (node:vm:309:38)
// at node:internal/process/execution:77:19
// at [eval]-wrapper:6:22
// at evalScript (node:internal/process/execution:76:60)
// at node:internal/main/eval_string:23:3
const name = 'Will Robinson';
console.warn(`Danger ${name}! Danger!`);
// Prints: Danger Will Robinson! Danger!, to stderr

Example using the Console class:

const out = getStreamSomehow();
const err = getStreamSomehow();
const myConsole = new console.Console(out, err);
myConsole.log('hello world');
// Prints: hello world, to out
myConsole.log('hello %s', 'world');
// Prints: hello world, to out
myConsole.error(new Error('Whoops, something bad happened'));
// Prints: [Error: Whoops, something bad happened], to err
const name = 'Will Robinson';
myConsole.warn(`Danger ${name}! Danger!`);
// Prints: Danger Will Robinson! Danger!, to err

@seesource

console
.
Console.log(message?: any, ...optionalParams: any[]): void (+1 overload)

Prints to stdout with newline. Multiple arguments can be passed, with the first used as the primary message and all additional used as substitution values similar to printf(3) (the arguments are all passed to util.format()).

const count = 5;
console.log('count: %d', count);
// Prints: count: 5, to stdout
console.log('count:', count);
// Prints: count: 5, to stdout

See util.format() for more information.

@sincev0.1.100

log
(
values: {
name: string;
email: string;
}
values
.
name: string
name
,
values: {
name: string;
email: string;
}
values
.
email: string
email
);
}}
/>
);
}

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

function
function App(): JSX.Element
App
() {
return (
<
function Form<{
name: string;
email: string;
}>({ initialValues, onSubmit }: FormProps<{
name: string;
email: string;
}>): JSX.Element
Form
FormProps<{ name: string; email: string; }>.initialValues: {
name: string;
email: string;
}
initialValues
={{
name: string
name
: "",
email: string
email
: "" }}
onSubmit={(
values: {
name: string;
email: string;
address: string;
}
values
: {
name: string
name
: string;
email: string
email
: string;
address: string
address
: string }) => {
Error ts(2322) ― Type '(values: { name: string; email: string; address: string; }) => void' is not assignable to type '(values: NoInfer<{ name: string; email: string; }>) => void'. Types of parameters 'values' and 'values' are incompatible. Property 'address' is missing in type '{ name: string; email: string; }' but required in type '{ name: string; email: string; address: string; }'.
var console: Console

The console module provides a simple debugging console that is similar to the JavaScript console mechanism provided by web browsers.

The module exports two specific components:

  • A Console class with methods such as console.log(), console.error() and console.warn() that can be used to write to any Node.js stream.
  • A global console instance configured to write to process.stdout and process.stderr. The global console can be used without importing the node:console module.

Warning: The global console object's methods are neither consistently synchronous like the browser APIs they resemble, nor are they consistently asynchronous like all other Node.js streams. See the note on process I/O for more information.

Example using the global console:

console.log('hello world');
// Prints: hello world, to stdout
console.log('hello %s', 'world');
// Prints: hello world, to stdout
console.error(new Error('Whoops, something bad happened'));
// Prints error message and stack trace to stderr:
// Error: Whoops, something bad happened
// at [eval]:5:15
// at Script.runInThisContext (node:vm:132:18)
// at Object.runInThisContext (node:vm:309:38)
// at node:internal/process/execution:77:19
// at [eval]-wrapper:6:22
// at evalScript (node:internal/process/execution:76:60)
// at node:internal/main/eval_string:23:3
const name = 'Will Robinson';
console.warn(`Danger ${name}! Danger!`);
// Prints: Danger Will Robinson! Danger!, to stderr

Example using the Console class:

const out = getStreamSomehow();
const err = getStreamSomehow();
const myConsole = new console.Console(out, err);
myConsole.log('hello world');
// Prints: hello world, to out
myConsole.log('hello %s', 'world');
// Prints: hello world, to out
myConsole.error(new Error('Whoops, something bad happened'));
// Prints: [Error: Whoops, something bad happened], to err
const name = 'Will Robinson';
myConsole.warn(`Danger ${name}! Danger!`);
// Prints: Danger Will Robinson! Danger!, to err

@seesource

console
.
Console.log(message?: any, ...optionalParams: any[]): void (+1 overload)

Prints to stdout with newline. Multiple arguments can be passed, with the first used as the primary message and all additional used as substitution values similar to printf(3) (the arguments are all passed to util.format()).

const count = 5;
console.log('count: %d', count);
// Prints: count: 5, to stdout
console.log('count:', count);
// Prints: count: 5, to stdout

See util.format() for more information.

@sincev0.1.100

log
(
values: {
name: string;
email: string;
address: string;
}
values
.
name: string
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:

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:

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:

This keeps your generics predictable.

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

Let's Discuss

Questions or feedback? Send me an email.

Last updated on

Back to blog