Reputation: 10939
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
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;
}
Upvotes: 2
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.
Upvotes: 4