Skip to content

Commit 2409294

Browse files
committed
Worked on objects chapter
1 parent 863d311 commit 2409294

File tree

1 file changed

+170
-55
lines changed

1 file changed

+170
-55
lines changed

book-content/chapters/06-objects.md

+170-55
Original file line numberDiff line numberDiff line change
@@ -1,31 +1,30 @@
11
# 06. Objects
22

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.
46

57
## Extending Objects
68

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.
810

911
### Intersection Types
1012

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.
1214

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.
1416

1517
Consider these types for `Album` and `SalesData`:
1618

1719
```typescript
1820
type Album = {
1921
title: string;
20-
2122
artist: string;
22-
2323
releaseYear: number;
2424
};
2525

2626
type SalesData = {
2727
unitsSold: number;
28-
2928
revenue: number;
3029
};
3130
```
@@ -38,49 +37,105 @@ type AlbumSales = Album & SalesData;
3837

3938
The `AlbumSales` type now requires objects to include all of the properties from both `AlbumDetails` and `SalesData`:
4039

41-
```tsx
42-
40+
```typescript
4341
const wishYouWereHereSales: AlbumSales = {
4442
title: "Wish You Were Here",
45-
4643
artist: "Pink Floyd",
47-
4844
releaseYear: 1975
49-
5045
unitsSold: 13000000,
51-
5246
revenue: 65000000,
5347
};
5448
```
5549

5650
If the contract of the `AlbumSales` type isn't fulfilled when creating a new object, TypeScript will raise an error.
5751

52+
It's also possible to intersect more than two types:
53+
54+
```typescript
55+
type AlbumSales = 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+
type StringAndNumber = 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+
type User1 = {
76+
age: number;
77+
};
78+
79+
type User2 = {
80+
age: string;
81+
};
82+
83+
type User = User1 & User2;
84+
85+
// hovering over User shows:
86+
type User = {
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+
5893
### Interfaces
5994

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?!".
6196

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.
6398

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:
65100

66101
```typescript
67-
interface Album {
102+
type Album = {
68103
title: string;
104+
artist: string;
105+
releaseYear: number;
106+
};
69107

108+
interface Album {
109+
title: string;
70110
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.
71118

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+
interface Album {
127+
title: string;
128+
artist: string;
72129
releaseYear: number;
73130
}
74131

75132
interface StudioAlbum extends Album {
76133
studio: string;
77-
78134
producer: string;
79135
}
80136

81137
interface LiveAlbum extends Album {
82138
concertVenue: string;
83-
84139
concertDate: Date;
85140
}
86141
```
@@ -90,25 +145,17 @@ This structure allows us to create more specific album representations with a cl
90145
```tsx
91146
const americanBeauty: StudioAlbum = {
92147
title: "American Beauty",
93-
94148
artist: "Grateful Dead",
95-
96149
releaseYear: 1970,
97-
98150
studio: "Wally Heider Studios",
99-
100151
producer: "Grateful Dead and Stephen Barncard",
101152
};
102153

103154
const oneFromTheVault: LiveAlbum = {
104155
title: "One from the Vault",
105-
106156
artist: "Grateful Dead",
107-
108157
releaseYear: 1991,
109-
110158
concertVenue: "Great American Music Hall",
111-
112159
concertDate: new Date("1975-08-13"),
113160
};
114161
```
@@ -121,48 +168,105 @@ interface BoxSet extends StudioAlbum, LiveAlbum {
121168
}
122169
```
123170

124-
### Types vs Interfaces
171+
### Intersections vs `interface extends`
172+
173+
We've now covered two separate TypeScript syntaxes for extending object types: `&` and `interface extends`. So, which is better?
174+
175+
You should choose `interface extends` for two reasons.
176+
177+
#### Better Errors When Merging Incompatible Types
178+
179+
We saw earlier that when you intersect two object types with an incompatible property, TypeScript will resolve the property to `never`:
180+
181+
```typescript
182+
type User1 = {
183+
age: number;
184+
};
185+
186+
type User2 = {
187+
age: string;
188+
};
189+
190+
type User = User1 & User2;
191+
```
192+
193+
When using `interface extends`, TypeScript will raise an error when you try to extend an interface with an incompatible property:
194+
195+
```typescript
196+
interface User1 {
197+
age: number;
198+
}
199+
200+
// Red line under User
201+
interface User extends User1 {
202+
age: string;
203+
}
204+
```
205+
206+
Hovering over `User` will show an error message:
207+
208+
```
209+
Interface 'User' incorrectly extends interface 'User1'.
210+
Types of property 'age' are incompatible.
211+
```
212+
213+
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.
125224

126-
We've now seen two ways of extending objects in TypeScript: one using `type` and one using `interface`.
225+
#### Conclusion
127226

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.
129228

130-
A `type` can represent anything– union types, objects, intersection types, and more.
229+
### Types Vs Interfaces
131230

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?
133232

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.
135234

136-
### Intersections vs Interfaces
235+
#### Types Can be Anything
137236

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.
139238

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+
type Union = string | number;
241+
```
141242

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.
143244

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).
145246

146-
### Interface Declaration Merging
247+
#### Declaration Merging
147248

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.
149250

150251
Here's an example of an `Album` interface with properties for the `title` and `artist`:
151252

152253
```typescript
153254
interface Album {
154255
title: string;
155-
156256
artist: string;
157257
}
158258
```
159259

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:
161261

162262
```typescript
163263
interface Album {
164-
releaseYear: number;
264+
title: string;
265+
artist: string;
266+
}
165267

268+
interface Album {
269+
releaseYear: number;
166270
genres: string[];
167271
}
168272
```
@@ -172,22 +276,39 @@ Behind the scenes, TypeScript automatically merges these two declarations into a
172276
```typescript
173277
interface Album {
174278
title: string;
279+
artist: string;
280+
releaseYear: number;
281+
genres: string[];
282+
}
283+
```
175284

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+
type Album = {
291+
title: string;
176292
artist: string;
293+
};
177294

295+
type Album = {
178296
releaseYear: number;
179-
180297
genres: string[];
181-
}
298+
};
182299
```
183300

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.
185304

186-
However, when using `interface` to declare a type, additional declarations are merged which may or may not be what you want.
305+
#### Conclusion
187306

188-
This might give you pause about using `interface` instead of `type` by default.
307+
So, should you use `type` or `interface` by default?
189308

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.
191312

192313
### Exercises
193314

@@ -198,21 +319,15 @@ Here we have a `User` type and a `Product` type, both with some common propertie
198319
```tsx
199320
type User = {
200321
id: string;
201-
202322
createdAt: Date;
203-
204323
name: string;
205-
206324
email: string;
207325
};
208326

209327
type Product = {
210328
id: string;
211-
212329
createdAt: Date;
213-
214330
name: string;
215-
216331
price: number;
217332
};
218333
```

0 commit comments

Comments
 (0)