Extending TypeScript Interfaces and Type Aliases with common properties
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'.
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 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!
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:
Here is a diagram showing how the type alias intersection works:
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>;
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;
};