I have following sample code that behaviour is not clear to me.
type A = {
field: string
}
type B = {
field: number
}
//I expect A | B is equivalent to field: A["field"] | B["field"]
type AORB = A | B
type AORBField = {
field: A["field"] | B["field"]
}
type BaseTypes = {
field: number | string
}
//Toggle between AORB and AORBField
type AB = AORB;
//type AB = AORBField;
//type AB = BaseTypes
const isA = (t: AB): t is A => {
return typeof t.field === "string"
}
const isB = (t: AB): t is B => {
return typeof t.field === "number"
}
const test = (x: AB) => {}
const testA = (x: A) => {}
const testB = (x: B) => {}
const testString = (x: string) => {}
const testNumber = (x: number) => {}
const getField = () => {
const val = Math.random();
return Math.random() % 2 ? val.toString(): val
}
const getDummyObject = () => {
const val = Math.random();
return { field: Math.random() % 2 ? val.toString(): val }
}
//Why error?
const ab: AB = {
//field: 1 //This will pass for AORB
//field: "string" //This will pass for AORB
field: getField() //This will pass for AORBFields
}
//Why error?
const abc: AB = getDummyObject();
if (isA(ab)){
const a: A = ab;
testString(a.field)
testNumber(a.field) //Expected err
testA(a)
test(ab)
}
else
{
const b: B = ab; //This will fail for AORBField and BaseTypes, but that is expected since else statement cannot figure out main type
testString(b.field) //Expected err
testNumber(b.field)
testB(b)
test(ab)
}
There is a typescript error on ab and abc assignment that is surprising to me. I expect following types are alike AORB = AORBField = BaseTypes at least in context of what we can assign to them. Obviously only AORB would correctly infer type on "If else" statement.
I've failed to find explanation in TS docs, could someone enlighten me here?
That isn't how a union of object types works, and for the most part it isn't how you'd want them to work (although your particular case may be an exception). The return type of your getDummyObject
is { field: string | number }
and you can't assign that to either A
or B
, so you can't assign that to the union A | B
either: it can't satisfy either type of the union. The two types don't "mix" the way you seem to want them to.
type A = {
field: number
}
type B = {
field: string
}
type AB = A | B
// return type of getDummyObject
type Neither = {
field: string | number
}
type Test1 = Neither extends A ? A : never // never
type Test2 = Neither extends B ? B : never // never
type Test3 = Neither extends AB ? AB : never // never
Which makes sense if you think about it, how could something that's neither an A
nor a B
be assigned to something that's either an A
or a B
?
If you change the code in getDummyObject
to be
const getDummyObject = () => {
const val = Math.random();
return Math.random() % 2 ?
{ field: val.toString() } :
{ field: val };
}
then it works just fine
const foo: AB = getDummyObject(); // no error