All Articles

Sometimes, Object Property Order Matters

Matt Pocock
Matt PocockMatt is a well-regarded TypeScript expert known for his ability to demystify complex TypeScript concepts.

Sometimes, the way you order your object properties matters.

The Setup

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),
(parameter) output: number
});

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),
(parameter) output: string
}); process({ produce: (input) => ({ value: input }), consume: (output) => console.log(output),
(parameter) output: { value: string; }
});

The Problem

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),
(parameter) output: unknown
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!

Could You Use 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),
(parameter) output: unknown
produce: (input) => Number(input), });

The result is still unknown. How frustrating!

Matt's signature

Share this article with your friends

`any` Considered Harmful, Except For These Cases

Discover when it's appropriate to use TypeScript's any type despite its risks. Learn about legitimate cases where any is necessary.

Matt Pocock
Matt Pocock

No, TypeScript Types Don't Exist At Runtime

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.

Matt Pocock
Matt Pocock

Deriving vs Decoupling: When NOT To Be A TypeScript Wizard

In this book teaser, we discuss deriving vs decoupling your types: when building relationships between your types or segregating them makes sense.

Matt Pocock
Matt Pocock