Back to Blog

Prevent TypeScript from Inferring

5 min read View on GitHub

I recently ran into an interesting TypeScript problem while refactoring a component at work. TypeScript’s type inference usually makes our lives easier, but sometimes it can be too helpful and infer from the wrong places.

In this post, I’ll walk through a simple form component, show how generics improve type safety, and then tackle what happens when inference goes too far. We’ll use the NoInfer utility type introduced in TypeScript 5.4 to fix it.

Note

This uses a form as a simplified example, but the pattern applies to any API with generics and callbacks.


Let’s start with a simple form component that 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 works, but when we use the component, we don’t get any type safety for the values parameter:

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
);
}}
/>
);
}

The values parameter is typed as any, so if the form shape changes or we mistype a property, TypeScript won’t catch it.

So let’s improve this by letting TypeScript infer the right types from the initial values we pass in.


Let TypeScript Infer Types

To make TypeScript infer the values type automatically, we can make our Form component generic. This way, the initial values type becomes the source of truth for everything else.

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 TypeScript will infer types based on the initial values we pass to the component:

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
}}
/>
);
}

The values parameter now has the accurate inferred type, which gives us autocomplete and type safety.

TypeScript will catch errors if we try to access a property that doesn’t exist:

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; }'.
}}
/>
);
}

We now have reliable type safety driven by the initial values we provide. But this power comes with a catch. Sometimes TypeScript infers too much.


When Inference Goes Too Far

So here’s where things got interesting during our refactor. We had a Form component with no generics, which meant developers had to manually type the callback parameters:

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
);
}}
/>
);
}

When we added generics to get type safety, those manual annotations became a problem:

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
);
}}
/>
);
}

Because initialValues is any, TypeScript infers T from the callback annotation instead. Now T is { name: string; email: string }, even though the actual initial values might have a different shape.

The inference happened, but from the wrong place. The callback annotation is now driving the types instead of the initial values. We needed a way to tell TypeScript: “Only infer T from initialValues, ignore the callback annotation.”

Stop Unwanted Inference with NoInfer

This is where NoInfer comes in. TypeScript 5.4 introduced this utility type specifically to handle situations like this. It tells TypeScript not to infer a type from specific positions, letting us control where inference comes from.

By wrapping the callback’s values parameter with NoInfer<T>, we tell TypeScript: “Don’t infer T from here, use the type inferred from elsewhere.”

Tip

If you’re on TypeScript versions before 5.4, you can create a similar effect with:

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

This uses tuple indexing to create a barrier that blocks inference while preserving the underlying 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: 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
/>;
}

Now when we use the component, TypeScript infers T only from initialValues, not from the callback:

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
);
}}
/>
);
}

The callback’s values parameter now has the correct type inferred from initialValues. If we try to add a manual annotation that doesn’t match, TypeScript will catch 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
: "" }}
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
);
}}
/>
);
}

NoInfer ensures that initialValues remains the source of truth for our types, even during refactoring when old manual annotations might still be present.

Behind the Scenes of NoInfer

If you’re curious how this actually works under the hood, NoInfer is implemented as an intrinsic type in TypeScript. Here’s how it’s defined in TypeScript’s standard library:

type NoInfer<T> = intrinsic;

Unlike regular utility types that are built using TypeScript’s type system, intrinsic types are special primitives that are implemented directly in the TypeScript compiler itself. The behavior of NoInfer is handled at the compiler level rather than through type manipulation.

When TypeScript sees NoInfer<T> during type inference:

  1. The compiler recognizes it as an intrinsic that blocks inference
  2. It skips this position when collecting inference candidates
  3. It uses the type inferred from other positions instead

The result: a type that’s structurally identical to T but invisible to TypeScript’s inference algorithm, giving you precise control over where inference happens.

Conclusion

We started with a form component that lost type safety because of any. By making the component generic, we let TypeScript infer types automatically from initialValues. But we also saw how inference can sometimes go too far, picking up types from the wrong place.

With the NoInfer utility type from TypeScript 5.4, we can control exactly where inference comes from, keeping initialValues as the single source of truth and maintaining consistent, predictable types.

Questions or Feedback?

I'd love to hear your thoughts, questions, or feedback about this post.

Reach me via email or LinkedIn.

Last updated on

Back to Blog