Sean Mackesey
Sean Mackesey

Reputation: 10939

Typescript changes type inference of return value depending on if there is a variable

I am confused by the below code:

type TestSymbol = 'a' | 'b';
type TestType = { test: TestSymbol }

function test1(): TestType {  // no error
  return { test: 'a' };
}

function test2(): TestType {  // tsserver error 2322
  const x = { test: 'a' };
  return x;
}

The error message is:

Type '{ test: string; }' is not assignable to type 'TestType'.
  Types of property 'test' are incompatible.
    Type 'string' is not assignable to type 'TestSymbol'.
[tsserver: 2322]

It appears typescript is classifying the test property of the return object differently in test1 and test2. In test1, it understands that the 'a' value is a TestSymbol. But in test2, it only classifies it as a string, causing the type error.

Why does it do this and how can I help typescript understand that test is a TestSymbol in test2?

Upvotes: 3

Views: 192

Answers (2)

akuiper
akuiper

Reputation: 214927

Because in test1, the object is directly returned and type is inferred based on the return type of the function. You can manually assert the type when using a variable so you don't rely on type inference:

function test2(): TestType {
  const x = { test: 'a' } as TestType;
  return x;
}

Or explicitly annotate the type of variable:

function test2(): TestType {
  const x: TestType = { test: 'a' };
  return x;
}

plaground

Upvotes: 2

jcalz
jcalz

Reputation: 327754

When there is no explicit type annotation on a variable or other expression, the TypeScript compiler will infer its type based on some heuristic rules that are a reasonable guess as to the intended type. These rules are not perfect, but they are useful.

In your first example,

function test1(): TestType {  
  return { test: 'a' };
}

the compiler infers the type of {test: 'a'} to be TestType contextually by the return type of the function. Contextual type inference means that the compiler infers the type of an expression based on what type is expected to be there. This only happens in specific circumstances, and cannot occur if a type has already been inferred for a value.

For example, when you declare a variable like:

const x = { test: 'a' };

the compiler immediately infers the type of x based on the value {test: 'a'}. It does not wait until it sees x used somewhere to figure out what the best type would be. The inferred type is {test: string}. Even though x is a const, the property x.test can be changed, so the compiler assumes that x.test can be any string. Often such assumptions are correct, but in your case it is not.


To fix this, you can either explicitly annotate the type of x:

function testAnnotation(): TestType {
  const x: TestType = { test: 'a' }; // <-- explicit annotation
  return x; // okay
}

or you can use a const assertion to change the inference heuristic so that the compiler assumes that nothing about x will change:

function testConstAssertion(): TestType {
  const x = { test: 'a' } as const; // <-- ask for narrowest possible type
  // const x: { readonly test: "a"; } 
  return x; // okay
}

The inferred type for x is now { readonly test: "a"; }; the compiler assumes that the test property will never change and that its only possible value is "a". This is seen as assignable to TestType (the fact that readonly properties are assignable to non-readonly properties is a little weird, but it's intended behavior and you can read a suggestion to change this at microsoft/TypeScript#13347) and there is no compiler error.

Either way should work.


Note that I would not recommend using a type assertion of the form:

function testLessSafeTypeAssertion(): TestType {
  const x = { test: 'a' } as TestType;
  return x;
}

While this will compile, it is less safe, because you are just telling the compiler that {test: "a"} is a TestType instead of asking the compiler to verify it. Type assertions tend to miss certain errors:

const x = { test: Math.random() < 0.5 ? 'a' : 'z' } as TestType; // no error!

where type annotations catch them:

const x: TestType = { test: Math.random() < 0.5 ? 'a' : 'z' }; // error

Type assertions are useful primarily in situations where the compiler cannot verify the type of a value, and you need to give the compiler the information it doesn't have. In this case the compiler can check that x is a TestType on its own, so an annotation or a const assertion is safer.


Playground link to code

Upvotes: 4

Related Questions