I understand that the union type in ts can allow variables to have multiple types.
type Test1 = never | any;
// Any is the top-level type
// Test1 = any
type Test2 = "123" | string;
// String is the collection of all strings
// Test2 = string
Type T=A | B, when A and B are combined
When A ("broad type") contains B ("specific type"), A ("broad type") will be concluded.
Then when I apply the conclusion of "object union", I have questions For parent-child type.
interface A1 {
name: string
}
interface A2 extends A1 {
age: number
}
type Test3 = A2 | A1;
// According to the conclusion, A2 is broader than A1, Test3 = A2
// Not equal to A2
/* Here I explain that when the value corresponding to Test3 is {name: "??"},
the mandatory item age attribute type in A2 type is unsafe
/
// According to the above explanation, I try to add options to A2. Will the result be different?
type Test4 = A1 | Partial<A2>;
// but, Not equal to A2 ?
type TypeKeys = keyof Test3;
// Why do I get such a result when I try to get the key
// TypeKeys = "name"
There are also questions when the application function returns
const record: Test3 = {
name: 'name',
age: 20
}
const record2: Test3 = {
name: 'name'
}
// Finally, I use Test3 type for function return
const fn = (): Test3 => record;
const da = fn();
da.name
da.age // The type shown here is unsafe
// Property 'age' does not exist on type 'Test3'.
// Property 'age' does not exist on type 'A1'
CodePudding user response:
When you use keyof Test3, TypeScript is trying to get the keys of the type Test3 which is A2 , so it gives you back the keys that are in A2 interface, which is "name" only.
you can solve this by using a type-guard, checking if the returned object is having the property of age,
const fn = (): Test3 => {
if(record.hasOwnProperty('age')){
return record;
}
return record2;
}
CodePudding user response:
There is a conceptual difference between Test2 and Test3:
"123"exists within thestringset already, i.e."123"is a subset of thestringset. So this union can effectively be collapsed into thestringsetA1is not a subset ofA2and vice versa, although this may seem counterintuitive at first glance:A1is an object with a single propertyname: stringA2is an object with two propertiesname: stringandage: number- There is no object that can be defined that can satisfy both of these definitions at the same time, therefore when you write
A1 | A2, the best the compiler can resolve to is that it could be eitherA1orA2, but certainly not both. - Note: This property is actually very powerful and allows us to leverage things like
discriminated unions
When you define record and record2, you are doing the following:
recordandrecord2are annotated asTest3, which is equivalent toA1 | A2.- You pass in an object of the shape of
A2torecord, which the compiler is perfectly happy with as this is a validA1 | A2. Importantly it is not thatrecordbecomesA2under the hood, it is stillA1 | A2 - You pass in an object of the shape of
A1torecord2, which the compiler is perfectly happy with as this is a validA1 | A2. - I find it easier to visualise if you imagine the variables defined as
letinstead ofconst; as long as the variable is assigned with something that is of the shape ofA1orA2during its lifetime, the compiler will remain happy (even if it starts asA2it could beA1in future etc)
When all is said and done, despite the contents of the objects in record and record2 obviously being A2 and A1 respectively to us, because of the annotation Test3 it is impossible for the compiler to deduce whether the underlying object is A1 or A2. All it can infer about Test3 is that regardless of the current value, it will have a name: string property. It cannot know whether the age: number property is present or missing as that would depend on knowledge about whether the object is A1 or A2.
A common solution to this problem is to "unpack" the type using a type guard, for example:
function isA2(record: Test3): record is A2 {
return (record as A2).age !== undefined;
}
function someFn() {
const someVariable: Test3 = { name: 'someName' };
if (isA2(someVariable)) {
someVariable // A2
someVariable.name // valid
someVariable.age // valid
}
else {
someVariable // A1
someVariable.name // valid
someVariable.age // invalid
}
}
This explicitly informs the compiler what the shape of the underlying type is using a runtime construct, so even if the variable's value were to change, it would still be able to guarantee type safety.
It should now hopefully make sense why the compiler did not accept accessing a property called age from a variable typed Test3 in your fn definition.
const record: Test3 = {
name: 'name',
age: 20
}
const fn = (): Test3 => record;
const da = fn();
da.name // valid, this property definitely exists
da.age // invalid, this property may or may not exist
The following alternatives would all be valid
const newRecord1: A2 = {
name: 'name',
age: 20
}
const fn1 = (): A2 => newRecord1;
const da1 = fn1();
da1.name // valid
da1.age // valid
const newRecord2: Test3 = {
name: 'name',
age: 20
}
const fn2 = (): Test3 => newRecord2;
const da2 = fn2();
if (isA2(da2)) {
da2.name // valid
da2.age // valid
}
