You signed in with another tab or window. Reload to refresh your session.You signed out in another tab or window. Reload to refresh your session.You switched accounts on another tab or window. Reload to refresh your session.Dismiss alert
Copy file name to clipboardExpand all lines: book-content/chapters/06-objects.md
+170-55
Original file line number
Diff line number
Diff line change
@@ -1,31 +1,30 @@
1
1
# 06. Objects
2
2
3
-
<!-- CONTINUE -->
3
+
So far, we've looked at object types only in the context of 'object literals', defined using `{}` with type aliases.
4
+
5
+
But TypeScript has many tools available that let you be more expressive with object types. You can model inheritance, create new object types from existing ones, and use dynamic keys.
4
6
5
7
## Extending Objects
6
8
7
-
Creating new objects based on objects that already exist is a great way to promote modularity and reusability in your code. There are two primary methods for extending objects in TypeScript: using intersections and extending interfaces.
9
+
Let's start our investigation by looking at how to build object types from _other object types_in TypeScript.
8
10
9
11
### Intersection Types
10
12
11
-
Unlike the union operator`|`which represents an "or" relationship between types, the intersection operator `&` signifies an 'and' relationship.
13
+
An intersection type lets us combine multiple object types into a single type. It uses the `&` operator. You can think of it like the reverse of the `|`operator. Instead of representing an "or" relationship between types, the `&`operator signifies an "and" relationship.
12
14
13
-
Using the intersection operator `&` combines multiple separate types into a single type that inherits all of the properties of the source types (formally known as "constituent types").
15
+
Using the intersection operator `&` combines multiple separate types into a single type.
14
16
15
17
Consider these types for `Album` and `SalesData`:
16
18
17
19
```typescript
18
20
typeAlbum= {
19
21
title:string;
20
-
21
22
artist:string;
22
-
23
23
releaseYear:number;
24
24
};
25
25
26
26
typeSalesData= {
27
27
unitsSold:number;
28
-
29
28
revenue:number;
30
29
};
31
30
```
@@ -38,49 +37,105 @@ type AlbumSales = Album & SalesData;
38
37
39
38
The `AlbumSales` type now requires objects to include all of the properties from both `AlbumDetails` and `SalesData`:
40
39
41
-
```tsx
42
-
40
+
```typescript
43
41
const wishYouWereHereSales:AlbumSales= {
44
42
title: "Wish You Were Here",
45
-
46
43
artist: "Pink Floyd",
47
-
48
44
releaseYear: 1975
49
-
50
45
unitsSold: 13000000,
51
-
52
46
revenue: 65000000,
53
47
};
54
48
```
55
49
56
50
If the contract of the `AlbumSales` type isn't fulfilled when creating a new object, TypeScript will raise an error.
57
51
52
+
It's also possible to intersect more than two types:
53
+
54
+
```typescript
55
+
typeAlbumSales=Album&SalesData& { genre:string };
56
+
```
57
+
58
+
This is a useful method for creating new types from existing ones.
59
+
60
+
#### Intersection Types With Primitives
61
+
62
+
It's worth noting that intersection types can also be used with primitives, like `string` and `number` - though it often produces odd results.
63
+
64
+
For instance, let's try intersecting `string` and `number`:
65
+
66
+
```typescript
67
+
typeStringAndNumber=string&number;
68
+
```
69
+
70
+
What type do you think `StringAndNumber` is? It's actually `never`. This is because `string` and `number` have innate properties that can't be combined together.
71
+
72
+
This also happens when you intersect two object types with an incompatible property:
73
+
74
+
```typescript
75
+
typeUser1= {
76
+
age:number;
77
+
};
78
+
79
+
typeUser2= {
80
+
age:string;
81
+
};
82
+
83
+
typeUser=User1&User2;
84
+
85
+
// hovering over User shows:
86
+
typeUser= {
87
+
age:never;
88
+
};
89
+
```
90
+
91
+
In this case, the `age` property resolves to `never` because it's impossible for a single property to be both a `number` and a `string`.
92
+
58
93
### Interfaces
59
94
60
-
<!-- TODO - introduce interfaces -->
95
+
So far, we've been only using the `type` keyword to define object types. Experienced TypeScript programmers will likely be tearing their hair out thinking "Why aren't we talking about interfaces?!".
61
96
62
-
Another option for creating new objects is to use TypeScript interfaces and the `extends` keyword. This approach is particularly useful when building hierarchies or when multiple extensions are needed.
97
+
Interfaces are one of TypeScript's most famous features. They shipped with the very first versions of TypeScript and are considered a core part of the language.
63
98
64
-
In this example, we have a base `Album` interface that will be extended into `StudioAlbum` and `LiveAlbum` interfaces that allow us to provide more specific details about an album:
99
+
Interfaces let you declare object types using a slightly different syntax to `type`. Let's compare the syntax:
65
100
66
101
```typescript
67
-
interfaceAlbum {
102
+
typeAlbum= {
68
103
title:string;
104
+
artist:string;
105
+
releaseYear:number;
106
+
};
69
107
108
+
interfaceAlbum {
109
+
title:string;
70
110
artist:string;
111
+
releaseYear:number;
112
+
}
113
+
```
114
+
115
+
They're largely identical, except for the keyword and an equals sign. But it's a common mistake to think of them as interchangeable. They're not.
116
+
117
+
They have quite different capabilities, which we'll explore in this section.
71
118
119
+
### `interface extends`
120
+
121
+
One of `interface`'s most powerful features is its ability to extend other interfaces. This allows you to create new interfaces that inherit properties from existing ones.
122
+
123
+
In this example, we have a base `Album` interface that will be extended into `StudioAlbum` and `LiveAlbum` interfaces that allow us to provide more specific details about an album:
124
+
125
+
```typescript
126
+
interfaceAlbum {
127
+
title:string;
128
+
artist:string;
72
129
releaseYear:number;
73
130
}
74
131
75
132
interfaceStudioAlbumextendsAlbum {
76
133
studio:string;
77
-
78
134
producer:string;
79
135
}
80
136
81
137
interfaceLiveAlbumextendsAlbum {
82
138
concertVenue:string;
83
-
84
139
concertDate:Date;
85
140
}
86
141
```
@@ -90,25 +145,17 @@ This structure allows us to create more specific album representations with a cl
This is very different because it actually sources an error. With intersections, TypeScript will only raise an error when you try to access the `age` property, not when you define it.
214
+
215
+
So, `interface extends` is better for catching errors when building out your types.
216
+
217
+
#### Better TypeScript Performance
218
+
219
+
When you're working in TypeScript, the performance of your types should be at the back of your mind. In large projects, how you define your types can have a big impact on how fast your IDE feels, and how long it takes for `tsc` to check your code.
220
+
221
+
`interface extends` is much better for TypeScript performance than intersections. With intersections, the intersection is recomputed every time it's used. This can be slow, especially when you're working with complex types.
222
+
223
+
But TypeScript can cache the resulting type of an interface based on its name. So if you use `interface extends`, TypeScript only has to compute the type once, and then it can reuse it every time you use the interface.
125
224
126
-
We've now seen two ways of extending objects in TypeScript: one using `type` and one using `interface`.
225
+
#### Conclusion
127
226
128
-
_So, what's the difference?_
227
+
`interface extends` is better for catching errors and for TypeScript performance. This doesn't mean you need to define all your object types using `interface` - we'll get to that later. But if you need to make one object type extend another, you should use `interface extends` where possible.
129
228
130
-
A `type` can represent anything– union types, objects, intersection types, and more.
229
+
### Types Vs Interfaces
131
230
132
-
On the other hand, an`interface` primarily represents object types, though it can also be used to define function types. Interfaces are particularly important given the significance of object types in TypeScript and the broader context of JavaScript.
231
+
Now we know how good`interface extends` is for extending object types, a natural question arises. Should we use `interface` for all our types by default?
133
232
134
-
Generally speaking, it doesn't matter too much if you use `type` or `interface` in your code, though you will learn benefits and drawbacks of each as you continue to work with TypeScript.
233
+
Let's look at a few comparison points between types and interfaces.
135
234
136
-
###Intersections vs Interfaces
235
+
#### Types Can be Anything
137
236
138
-
For the topic at hand, when it comes to extending objects you'll get better performance from using `interface extends`.
237
+
Type aliases are a lot more flexible than interfaces. A `type` can represent anything– union types, objects, intersection types, and more.
139
238
140
-
By using `interface extends`, TypeScript can cache interfaces based on their names. This effectively creates a reference for future use. It's also more expressive when reading the code, as it's clear that one interface is extending another.
239
+
```typescript
240
+
typeUnion=string|number;
241
+
```
141
242
142
-
In contrast, using `&` requires TypeScript to compute the intersection almost every time it's used. This is largely due to the potentially complex nature of intersections.
243
+
When we declare a type alias, we're just giving an alias to an existing type.
143
244
144
-
The debate between `type` vs`interface`will continue to arise in the TypeScript community, but when extending objects, using interfaces and the `extends` keyword is the way to go.
245
+
On the other hand, an`interface`can only represent object types (and functions, which we'll look at much later).
145
246
146
-
###Interface Declaration Merging
247
+
####Declaration Merging
147
248
148
-
In addition to being able to `extend` interfaces, TypeScript also allows for interfaces to be declared more than once. When an interface is declared multiple times, TypeScript automatically merges the declarations into a single interface. This is known as interface declaration merging.
249
+
Interfaces in TypeScript have an odd property. When multiple interfaces with the same name in the same scope are created, TypeScript automatically merges them. This is known as declaration merging.
149
250
150
251
Here's an example of an `Album` interface with properties for the `title` and `artist`:
151
252
152
253
```typescript
153
254
interfaceAlbum {
154
255
title:string;
155
-
156
256
artist:string;
157
257
}
158
258
```
159
259
160
-
Suppose that as the application evolves, you want to add more details to the album's metadata, such as the release year and genre. With declaration merging, you can simply declare the `Album` interface again with the new properties:
260
+
Suppose that as the application evolves, you want to add more details to the album's metadata, such as the release year and genre. With declaration merging, you can declare the `Album` interface _again_ with the new properties:
161
261
162
262
```typescript
163
263
interfaceAlbum {
164
-
releaseYear:number;
264
+
title:string;
265
+
artist:string;
266
+
}
165
267
268
+
interfaceAlbum {
269
+
releaseYear:number;
166
270
genres:string[];
167
271
}
168
272
```
@@ -172,22 +276,39 @@ Behind the scenes, TypeScript automatically merges these two declarations into a
172
276
```typescript
173
277
interfaceAlbum {
174
278
title:string;
279
+
artist:string;
280
+
releaseYear:number;
281
+
genres:string[];
282
+
}
283
+
```
175
284
285
+
This is very different from `type`, which would give you an error if you tried to declare the same type twice:
286
+
287
+
```typescript
288
+
// Red line under both Album's
289
+
// Duplicate identifier 'Album'
290
+
typeAlbum= {
291
+
title:string;
176
292
artist:string;
293
+
};
177
294
295
+
typeAlbum= {
178
296
releaseYear:number;
179
-
180
297
genres:string[];
181
-
}
298
+
};
182
299
```
183
300
184
-
This is a different behavior than in JavaScript, which would cause an error if you tried to redeclare an object in the same scope. You would also get an error in TypeScript if you tried to redeclare a type with the `type` keyword.
301
+
Coming from a JavaScript point of view, this behavior of interfaces feels pretty weird. I have lost hours of my life to having two interfaces with the same name in the same 2,000+ line file. It's there for a good reason - that we'll explore in a later chapter - but it's a bit of a gotcha.
302
+
303
+
This alone makes me wary of using interfaces by default.
185
304
186
-
However, when using `interface` to declare a type, additional declarations are merged which may or may not be what you want.
305
+
#### Conclusion
187
306
188
-
This might give you pause about using `interface` instead of `type` by default.
307
+
So, should you use `type` or `interface` by default?
189
308
190
-
There will be more advanced examples of interface declaration merging in the future, but for now, it's important to know that TypeScript automatically merges interfaces when they're declared multiple times.
309
+
I tend to default to `type` unless I need to use `interface extends`. This is because `type` is more flexible and doesn't have the weirdness of declaration merging.
310
+
311
+
But, it's a close call. I wouldn't blame you for going the opposite way. Many folks coming from a more object-oriented background will prefer `interface` because it's more familiar to them. And, as we'll see later, `interface` works particularly well with classes.
191
312
192
313
### Exercises
193
314
@@ -198,21 +319,15 @@ Here we have a `User` type and a `Product` type, both with some common propertie
0 commit comments