
Overriding Properties when Extending Objects
The Problem
Try this in your editor:
1interface IBaseModel {
2 id: string;
3 created: Date;
4 updated: Date;
5}
6interface IStudent extends IBaseModel {
7 id: number; //override type
8 name: string;
9}
[View in Playground]
in case you don't want to type it yourself :-)
You get an error with the second interface declaration (Student
):
InterfaceStudent
incorrectly extends interfaceIBaseModel
. Types of propertyid
are incompatible. Typenumber
is not assignable to typestring
.
In other words, "you cannot change the type of a property in the interface from which you are extending your new interface."
A harder problem to debug...
Now try this in your editor:
1type StudentBase = {
2 kind: "StudentBase";
3 name: string;
4}
5
6type Student = StudentBase & {
7 kind: "Student";
8 email: string;
9}
10
11function printStudent(student: Student) {
12 console.log(student);
13}
14
15printStudent({
16 kind: "Student",
17 name: "Roger",
18 email: "roger@school.org"
19});
This time, you get errors on the three properties (kind
, name
, and email
) in the Student
argument you passed into printStudent()
. The text of each error is "Type 'string' is not assignable to type never
".
What is going on here? We can see that each of the properties has a string type.
The issue is when we intersect two objects, (e.g. A & B
) we end up with an object that has all of the properties of A
and all of the properties of B
. In our example above, the resulting type for Student
would be...
1type Student = {
2 //the stuff from StudentBase
3 kind: "StudentBase";
4 name: string;
5 //the stuff added
6 kind: "Student";
7 email: string;
8}
As you can see, we have two definitions for the property kind
and it is impossible for its value to be "StudentBase" and "Student" at the same time. Thus, Student
type is never
: no value can ever satisfy its definition.
An easier way of looking at this is:type Fancy = string & number;
Can you think of any value that is both a string and a number? "14" !== 14
Solution
So how can we extend interfaces and intersect types that have properties in common?
Answer: simple - just omit the common property from the interface/type that you want to override.
1interface IBaseModel {
2 id: string;
3 created: Date;
4 updated: Date;
5}
6interface IStudent extends Omit<IBaseModel, 'id'> {
7 id: number; //override type
8 name: string;
9}
10
11type StudentBase = {
12 kind: "StudentBase";
13 name: string;
14}
15
16type Student = Omit<StudentBase, 'kind'> & {
17 kind: "Student";
18 email: string;
19}
20
21function printStudent(student: Student) {
22 console.log(student);
23}
24
25printStudent({
26 kind: "Student",
27 name: "Roger",
28 email: "roger@school.org"
29});
Follow up
Recently, I came across this SO answer to the question "How to override type properties in Typescript". The author suggested creating a helper type:
1type Overwrite<T, U> = Pick<T, Exclude<keyof T, keyof U>> & U;
Let's break it down. Essentially, the helper type returns the intersection of two types, but the everything in the first type except those properties that are in the second type.
Pick<T, (these)>
... Pick these properties of T (the first type)
Exclude<keyof T, keyof U>
... Exclude<A, B>
takes two union types (A and B) and returns all union members of A not in B.
Example: Exclude<'a'|'b'|'c', 'c'|'d'>
results in 'a'|'b'
Adding in the keyof
1type Obj1 = { a: string, b: string, c: string };
2type Obj2 = { c: number, d: number };
3type Test2 = Exclude<keyof Obj1, keyof Obj2>;
4// ^? Test2 = "a" | "b"
Combining it with Pick<T, (these)>
1type Obj1 = { a: string, b: string, c: string };
2type Obj2 = { c: number, d: number };
3type Test3 = Pick<Obj1, Exclude<keyof Obj1, keyof Obj2>>
4// ^? Test3 = { a: string, b: string }
And throw in the intersection...
1type Obj1 = { a: string, b: string, c: string };
2type Obj2 = { c: number, d: number };
3type Test4 = Pick<Obj1, Exclude<keyof Obj1, keyof Obj2>> & Obj2;
4// ^? Test4 = { a: string, b: string, c: number, d: number }
[Playground for all of these examples]
Takes time to sink in...
My initial reaction was "very cool, but my Omit<>
strategy works just fine." In fact, it wasn't until I reviewed the SO answer for this blog post that I began to realize just how useful the helper really is.
First off, using Overwrite<T, U>
has the advantage that you do not have to list the properties that you want to Omit<>
. It automatically uses the common properties from the second object type (U).
Second, it is more intuitive than using the intercept operator (`&`) - at least for me.
I have changed its name to Override<T, U>
but that might change as I continue to ponder this find approach to intersecting two object types with common property names.