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_ais optional - The overridden
prop_bis required - The original
prop_cis not changed - The new required
prop_newwas 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;
};