Extending TypeScript Interfaces and Type Aliases with common properties

posted on December 21, 2022

In this article, I want to discuss what happens when extending TypeScript Interfaces and intersecting Type Aliases that have common properties of different types.

Extending Interfaces

Let's start with interfaces first. Assume an interface IBase that has an optional property prop_b:

interface IBase {
  prop_a: string;
  prop_b?: string;
  proc_c: string;
}

Let's say we want to extend this interface with another interface that adds a new property prop_new but also changes the property prop_b to a required property and prop_a to an optional property:

interface IExtendBase extends IBase {
    prop_a?: string;
    prop_b: string;
    prop_new: string;
}

If you try that, you will see the following TypeScript error:

Interface 'IExtendBase' incorrectly extends interface 'IBase'.
  Types of property 'prop_a' are incompatible.
    Type 'string | undefined' is not assignable to type 'string'.
      Type 'undefined' is not assignable to type 'string'.
Extending an interface with an optional property that overrides a required property throws an error.
Extending an interface with an optional property that overrides a required property throws an error.

As the error suggests, this happens because the property prop_a in the IExtendBase interface cannot override the same property in the extended interface IBase. The prop_a in the IExtendBase has a broader set of possible value types than the same property in the IBase interface. By the way, this would also happen if the prop_a property in the IExtendBase wouldn't be optional but had a broader set of value types. For example, having a union of string and number would also throw an error:

interface IExtendBase extends IBase {
    prop_a: string | number;
    prop_b: string;
    prop_new: string;
}
Interface 'IExtendBase' incorrectly extends interface 'IBase'.
  Types of property 'prop_a' are incompatible.
    Type 'string | number' is not assignable to type 'string'.
      Type 'number' is not assignable to type 'string'.

To fix this problem, we need to omit the prop_a property from the IBase interface before extending it:

interface IExtendBase extends Omit<IBase, 'prop_a'> {
    prop_a?: string;
    prop_b: string;
    prop_new: string;
}

Now the new extended interface will have the properties the way we want them:

  • The overridden prop_a is optional
  • The overridden prop_b is required
  • The original prop_c is not changed
  • The new required prop_new was added
Extending an interface with an optional property while omitting the original required property.
Extending an interface with an optional property while omitting the original required property.

Playground Link

Extending Type Aliases

Type aliases behave similarly to interfaces. However, if we try to achieve similar behavior with types, we won't get an error message as we did before.

type TBase = {
  prop_a: string;
  prop_b?: string;
  prop_c: string;
}

type TExtendBase = TBase & {
    prop_a?: string;
    prop_b: string;
    prop_new: string;
};

Instead, the prop_a in the TExtendBase type will be required, not optional!

Extending type alias with optional property leaves it required.
Extending type alias with optional property leaves it required.

Playground Link

This happens because the intersection (&) of two types creates a new type with all the properties included in the intersected types (union of property names). In contrast, the type of every property is an intersection of its value types in the intersected types (intersection of property types). Moreover, if the prop_a property in the TExtendBase type would have a broader set of value types, they would be intersected with the string type from the TBase type leaving a string type:

Extending type alias with optional property leaves it as string
Extending type alias with optional property leaves it as string

Here is a diagram showing how the type alias intersection works:

TypeScript type alias intersection diagram
TypeScript type alias intersection diagram (View large version)

Interestingly, a naive intersection logic can be achieved using the following type:

type TypeA = {
    prop_a: string | boolean;
    prop_b: string | number;
    prop_c?: string;
    prop_d: string;
}

type TypeB = {
    prop_a: boolean | number;
    prop_b: number | boolean;
    prop_c: string;
    prop_e: string;
}

type IntersectTypes<Type1, Type2> = {
    [Property in (keyof Type1 | keyof Type2)]:
    Property extends (keyof Type1 & keyof Type2)
        ? Type1[Property] & Type2[Property]
        : Property extends Exclude<keyof Type1, keyof Type2>
            ? Type1[Property]
            : Property extends Exclude<keyof Type2, keyof Type1>
                ? Type2[Property]
                : never;
}

type TypeC = IntersectTypes<TypeA, TypeB>;
TypeScript custom intersection type
TypeScript custom intersection type

The fix

Just like with interfaces, to fix this problem, we need to omit the prop_a property from the TBase type before intersecting it:

type TBase = {
  prop_a: string;
  prop_b?: string;
  prop_c: string;
}

type TExtendBase = Omit<TBase, 'prop_a'> & {
    prop_a?: string;
    prop_b: string;
    prop_new: string;
};
Extending type alias with an optional property while omitting the extended property
Extending type alias with an optional property while omitting the extended property

Playground Link