Announcing: A Free Book, A New Course, A Huge Price Cut...
It's a massive ship day. We're launching a free TypeScript book, new course, giveaway, price cut, and sale.
Sometimes, the way you order your object properties matters.
Let's imagine we create a function that takes in an object as its argument. In this object, there are two properties: produce
and consume
.
process ({
produce : (input ) => Number (input ),
consume : (output ) => console .log (output ),});
produce
takes in an input of string
and returns some type. consume
then takes in that value and does something with it. Because of a clever type definition, TypeScript can infer the type of the value returned by produce
and pass it to consume
:
const process = <T >(obj : {
produce : (input : string) => T ;
consume : (t : T ) => void;
}) => {
const value = obj .produce ("abc");
obj .consume (value );
};
We can use this setup with any type of value, and it'll just work:
process ({
produce : (input ) => input + "hello",
consume : (output ) => console .log (output ),});
process ({
produce : (input ) => ({ value : input }),
consume : (output ) => console .log (output ),});
This all looks great, until one of our users complains to us. They're trying to use our function, but it's not working:
process ({
consume : (output ) => console .log (output ), produce : (input ) => Number (input ),
});
The output
is being seen by TypeScript as unknown
. This feels very odd, as the produce
function is clearly returning a number
. What's going on?
The difference here is that the user specified consume
before produce
. Since TypeScript 4.7, in this PR, TypeScript now uses the order of properties to inform its inference. This was added to fix various long-standing bugs (linked in the PR) with context-sensitive functions.
This means that, in some very narrow cases, the order you specify your properties matters. So if you're running up against strange errors to do with property ordering, that's why!
NoInfer
?You might be wondering if you could use NoInfer
to fix this. That seems sensible, given that NoInfer
is used to force TypeScript to avoid inference on certain targets.
However, it doesn't:
const process = <T >(obj : {
produce : (input : string) => T ;
consume : NoInfer <(t : T ) => void>;
}) => {
const value = obj .produce ("abc");
obj .consume (value );
};
process ({
consume : (output ) => console .log (output ), produce : (input ) => Number (input ),
});
The result is still unknown
. How frustrating!
Share this article with your friends
It's a massive ship day. We're launching a free TypeScript book, new course, giveaway, price cut, and sale.
Learn how to use corepack
to configure package managers in Node.js projects, ensuring you always use the correct one.
Learn how to strongly type process.env in TypeScript by either augmenting global type or validating it at runtime with t3-env.
Discover when it's appropriate to use TypeScript's any
type despite its risks. Learn about legitimate cases where any
is necessary.
Learn why TypeScript's types don't exist at runtime. Discover how TypeScript compiles down to JavaScript and how it differs from other strongly-typed languages.
Improve React TypeScript performance by replacing type & with interface extends. Boost IDE and tsc speed significantly.