Home > Software engineering >  Typescript - Union Type of base interface and extended interface
Typescript - Union Type of base interface and extended interface

Time:07-12

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

  1. why is the type A i defined different from the type A i expected to get?
  2. 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)
}

Playground

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.

Playground link

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,
};

Playground link

...or use as const to tell TypeScript that the object will never be modified:

const a = {
    a: 5,
    b: 5,
} as const;

Playground link

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;

Playground link

  • Related