I have the following code
interface BaseA {
a: number;
}
interface SpecialA extends BaseA {
b: number;
}
type A = BaseA | SpecialA
const a = {
a: 5, b: 5
} as A
console.log(a.b)
I expected the code to be valid, but im getting the error
Property 'b' does not exist on type 'A'.
Property 'b' does not exist on type 'BaseA'
It seems the type A is not what i was trying to define, i was expecting it to be equivelent to the following type
interface A {
a: number;
b?: number;
}
My questions are
- why is the type A i defined different from the type A i expected to get?
- how can i define the expected type A without defining it by hand?
Note: i need to use the type SpecialA as is in some places, so not defining it and just defining expected A is not an option.
CodePudding user response:
Since A
is a union, you need to narrow/discriminate your type to access non shared members.
interface BaseA {
a: number;
}
interface SpecialA extends BaseA {
b: number;
}
type A = BaseA | SpecialA
const a = {
a: 5, b: 5
} as A
if ("b" in a) { // narrowing here
console.log(a.b)
}
CodePudding user response:
By using as A
, you've said "The variable a
may contain a BaseA
or a SpecialA
." Later, you tried to use a.b
without any kind of type check, but since a
may be just a BaseA
(not a SpecialA
), that's a type error. TypeScript doesn't know that what a
refers to has a b
property; it may be just a BaseA
, which doesn't have b
.
You can resolve that by checking first:
if ("b" in a) {
console.log(a.b);
}
Within the if
block, TypeScript knows that a
refers to SpecialA
.
You might be thinking: But a
is a constant, not a variable. That's true, but the object the constant refers to can be modified to no longer have a b
property:
delete (a as any).b; // blech
If you're never going to remove the b
property, either just use SpecialA
as the type:
const a: SpecialA = {
a: 5,
b: 5,
};
...or use as const
to tell TypeScript that the object will never be modified:
const a = {
a: 5,
b: 5,
} as const;
Note that if you do the as const
thing, there's no need for a type annotation on it at all. TypeScript's type checking is structural (based on the shape of things, what properties they have and their types), not nominal (based on the names of types). But you could use the type A
on it for some clarity for other programmers:
const a: A = {
// ^^^
a: 5,
b: 5,
} as const;