I've been scratching my head and searching the internet for an explanation to this.
I started with my full code and boiled it down to the following that illustrates my problem:
function foo (param: string) {
if (param === 'bar') {
return {
one: 'bar'
}
}
return {
two: 'baz'
}
}
Why does this give me:
declare function foo(param: string): {
one: string;
two?: undefined;
} | {
two: string;
one?: undefined;
};
and not
declare function foo(param: string): {
one: string;
} | {
two: string;
};
At first I thought it was a difference between functions and arrow functions, but this issue taught me otherwise and pointed me in the direction that it might be an inference issue.
Then I came across Distributive Conditional Types and thought it might have something to do with it but having read this answer (and hopefully understood it correctly) made me think that it was not.
I have now read a lot of other resources too, but I can't figure out what I'm missing or why I can't put all the pieces together to form an understanding of this.
CodePudding user response:
There is no problem here; the type inferred by Typescript here is more useful than the one you propose.
An object type like {one: string}
does not mean that an object of that type can only have that one property. This type only means the object must have that property, with no restriction on what other properties the object has. In contrast, a type like {one: string, two?: undefined}
means that the object has the property one
and does not have the property two
, except possibly with the value undefined
.
This means that the inferred type is more specific: the object either has one
and not two
, or it has two
and not one
. That's more informative than just saying the object either has one
or two
, because that also means it could have both.
Also note here that when you access an object's properties, Typescript will give you an error if the property is not declared on that type. For example, accessing the property obj.two
when obj
has type {one: string} | {two: string}
will give a type error, and it should: an object of this type could look like {one: 'fish', two: 3.14}
, i.e. its two
property could be literally anything.
On the other hand, {one: string, two?: undefined} | {one?: undefined, two: string}
has the property two
in both branches, so there is no type error when you access obj.two
. This is because Typescript can know the type of obj.two
- it's definitely either string
or undefined
, it cannot be anything else.
type Foo = {one: string, two?: undefined} | {two: string, one?: undefined}
// error, as desired
let foo: Foo = {one: 'fish', two: 'fish'};
// ok
foo.one;
type Bar = {one: string} | {two: string}
// object is wrong, but no error :-(
let bar: Bar = {one: 'fish', two: 'fish'};
// error when trying to use the object in a sensible way :-(
bar.one;
CodePudding user response:
Actually, the type:
{
one: string;
two?: undefined;
} | {
two: string;
one?: undefined;
}
is the exact same as*: (Exact Optional Property Types, an optional flag, changes this)
{
one: string;
} | {
two: string;
}
since key?: undefined
means a non-existent key. The reason why it returns this type is to allow you to do guard checking on the property:
function test(bool: boolean) {
if (bool)
return { one: "bar" }
else
return { two: "baz" }
}
if (test(true).one) {
// this wouldn't work otherwise
}
function test2(bool: boolean): { one: string } | { two: string } {
if (bool)
return { one: "bar" }
else
return { two: "baz" }
}
if (test2(true).one) {
// as shown!
}